feat: 项目初始化

This commit is contained in:
2025-10-30 16:24:07 +08:00
parent 5060a93614
commit 7321a9e172
291 changed files with 23872 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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cutejava</artifactId>
<groupId>cn.odboy</groupId>
<version>1.4.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cutejava-framework</artifactId>
<name>公共模块</name>
</project>
@@ -0,0 +1,29 @@
/*
* Copyright 2021-2025 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.*;
/**
* 用于标记匿名访问方法
*/
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
@@ -0,0 +1,75 @@
/*
* Copyright 2021-2025 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 org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* 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,75 @@
/*
* Copyright 2021-2025 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 org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* 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,75 @@
/*
* Copyright 2021-2025 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 org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* 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,75 @@
/*
* Copyright 2021-2025 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 org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* 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,75 @@
/*
* Copyright 2021-2025 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 org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* 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,44 @@
/*
* Copyright 2021-2025 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 lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
/**
* 通用参数
*
* @author odboy
* @date 2025-07-23
*/
public class CsArgs {
@Getter
@Setter
public static class FindByLongId extends CsObject {
@NotNull(message = "id必填")
private Long id;
}
@Getter
@Setter
public static class DeleteById extends CsObject {
@NotNull(message = "id必填")
private Long id;
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2021-2025 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 lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class CsBaseCreateTimeTb extends CsObject {
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date createTime;
}
@@ -0,0 +1,39 @@
/*
* Copyright 2021-2025 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 lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class CsBaseTimeTb extends CsObject {
@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,35 @@
/*
* Copyright 2021-2025 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 lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class CsBaseUpdateTimeTb extends CsObject {
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date updateTime;
}
@@ -0,0 +1,67 @@
/*
* Copyright 2021-2025 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 lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.data.annotation.CreatedBy;
import java.lang.reflect.Field;
import java.util.Date;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class CsBaseUserCreateTimeTb extends CsObject {
@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;
/* 分组校验 */
public @interface Create {
}
/* 分组校验 */
public @interface Update {
}
@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();
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2021-2025 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 lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import java.util.Date;
/**
* 逻辑删除通用模型
*
* @author odboy
* @date 2025-01-12
*/
@Getter
@Setter
public class CsBaseUserTimeLogicTb extends CsObject {
@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,77 @@
/*
* Copyright 2021-2025 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 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;
import java.lang.reflect.Field;
import java.util.Date;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class CsBaseUserTimeTb extends CsObject {
@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;
/* 分组校验 */
public @interface Create {
}
/* 分组校验 */
public @interface Update {
}
@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();
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2021-2025 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 CsObject implements Serializable {
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,47 @@
/*
* Copyright 2021-2025 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 io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 数据分页参数
*
* @author odboy
* @date 2025-01-20
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CsPageArgs<T> implements Serializable {
@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;
}
@@ -0,0 +1,52 @@
/*
* Copyright 2021-2025 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 lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 结果封装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class CsPageResult<T> extends CsObject {
private List<T> content;
private long totalElements;
public static <T> CsPageResult<T> emptyListData() {
CsPageResult<T> result = new CsPageResult<>();
result.setTotalElements(0);
result.setContent(new ArrayList<>());
return result;
}
public static <T> CsPageResult<T> listData(IPage<T> page) {
CsPageResult<T> result = new CsPageResult<>();
result.setTotalElements(page.getTotal());
result.setContent(page.getRecords());
return result;
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2021-2025 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 lombok.*;
import java.util.Map;
/**
* 元数据选项
*
* @author odboy
* @date 2025-01-13
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class CsSelectOptionVo extends CsObject {
private String label;
private String value;
private Map<String, Object> ext;
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2025 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,34 @@
/*
* Copyright 2021-2025 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,69 @@
/*
* Copyright 2021-2025 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,53 @@
/*
* Copyright 2021-2025 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";
}
@@ -0,0 +1,37 @@
/*
* Copyright 2021-2025 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,54 @@
/*
* Copyright 2021-2025 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 lombok.extern.slf4j.Slf4j;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 主要目的是输出应用的服务地址
*
* @author odboy
* @date 2024-04-20
*/
@Slf4j
public class BootApplication {
@Bean
public CsSpringBeanHolder springBeanHolder() {
return new CsSpringBeanHolder();
}
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(
"\n----------------------------------------------------------\n\t" +
"Application is running! Access URLs:\n\t" + "Local: \t\thttp://localhost:" + port + path + "/\n\t" +
"External: \thttp://" + ip + ":" + port + path + "/\n\t" + "Swagger文档: \thttp://" + ip + ":" + port +
path + "/doc.html\n" + "----------------------------------------------------------");
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2021-2025 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 org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 获取 HttpServletRequest
*/
public class CsRequestHolder {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2021-2025 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.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 脚本工具
*
* @author odboy
* @date 2025-09-23
*/
@Slf4j
@Component
public class CsScriptHelper {
public Object evalObj(String script, Map<String, Object> args) {
try (Context rhino = Context.enter()) {
Scriptable scope = rhino.initStandardObjects();
bindArgumentsToScope(scope, args);
Object result = rhino.evaluateString(scope, script, "JavaScript", 1, null);
log.info("执行结果: {}", Context.toString(result));
return result;
}
}
private void bindArgumentsToScope(Scriptable scope, Map<?, ?> args) {
if (args == null) {
return;
}
for (Map.Entry<?, ?> entry : args.entrySet()) {
Object jsValue = Context.javaToJS(entry.getValue(), scope);
scope.put(entry.getKey().toString(), scope, jsValue);
}
}
private void bindArgumentsToScope(Scriptable scope, Object args) {
if (args == null) {
return;
}
// 单个对象参数
Object jsValue = Context.javaToJS(args, scope);
scope.put("args", scope, jsValue);
}
}
@@ -0,0 +1,172 @@
/*
* Copyright 2021-2025 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.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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
@SuppressWarnings({"unchecked", "all"})
public class CsSpringBeanHolder implements ApplicationContextAware, DisposableBean {
private static ApplicationContext applicationContext = null;
private static final List<CallBack> CALL_BACKS = new ArrayList<>();
private static boolean addCallback = true;
/**
* 针对 某些初始化方法, 在SpringContextHolder 未初始化时 提交回调方法。 在SpringContextHolder 初始化后, 进行回调使用
*
* @param callBack 回调函数
*/
public synchronized static void addCallBacks(CallBack callBack) {
if (addCallback) {
CsSpringBeanHolder.CALL_BACKS.add(callBack);
} else {
log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName());
callBack.executor();
}
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
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;
}
@Override
public void destroy() {
CsSpringBeanHolder.clearHolder();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (CsSpringBeanHolder.applicationContext != null) {
log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" +
CsSpringBeanHolder.applicationContext);
}
CsSpringBeanHolder.applicationContext = applicationContext;
if (addCallback) {
for (CallBack callBack : CsSpringBeanHolder.CALL_BACKS) {
callBack.executor();
}
CALL_BACKS.clear();
}
CsSpringBeanHolder.addCallback = false;
}
/**
* 获取 @Service 的所有 bean 名称
*
* @return /
*/
public static List<String> getAllServiceBeanName() {
return new ArrayList<>(Arrays.asList(applicationContext.getBeanNamesForAnnotation(Service.class)));
}
interface CallBack {
/**
* 回调执行方法
*/
void executor();
/**
* 本回调任务名称
*
* @return /
*/
default String getCallBackName() {
return Thread.currentThread().getId() + ":" + this.getClass().getName();
}
}
}
@@ -0,0 +1,154 @@
/*
* Copyright 2021-2025 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.doc;
import cn.odboy.constant.SystemConst;
import cn.odboy.framework.properties.AppProperties;
import cn.odboy.util.CsAnonTagUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* api页面 /doc.html
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${server.servlet.context-path:}")
private String apiPath;
@Autowired
private AppProperties properties;
@Autowired
private ApplicationContext applicationContext;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2).enable(properties.getSwagger().getEnabled())
.pathMapping("/")
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.regex("^(?!/error).*"))
.paths(PathSelectors.any())
.build()
//添加登陆认证
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().description("一个简单且易上手的自动化运维平台")
.title("CuteJava 接口文档")
.version("1.4.1")
.build();
}
private List<SecurityScheme> securitySchemes() {
//设置请求头信息
List<SecurityScheme> securitySchemes = new ArrayList<>();
ApiKey apiKey = new ApiKey(SystemConst.HEADER_NAME, SystemConst.HEADER_NAME, "header");
securitySchemes.add(apiKey);
return securitySchemes;
}
private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(getContextByPath());
return securityContexts;
}
private SecurityContext getContextByPath() {
Set<String> urls = CsAnonTagUtil.getAllAnonymousUrl(applicationContext);
urls = urls.stream().filter(url -> !"/".equals(url)).collect(Collectors.toSet());
String regExp = "^(?!" + apiPath + String.join("|" + apiPath, urls) + ").*$";
return SecurityContext.builder().securityReferences(defaultAuth()).operationSelector(o -> o.requestMappingPattern()
// 排除不需要认证的接口
.matches(regExp)).build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> securityReferences = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
securityReferences.add(new SecurityReference(SystemConst.HEADER_NAME, authorizationScopes));
return securityReferences;
}
/**
* 解决Springfox与SpringBoot集成后, WebMvcRequestHandlerProvider和WebFluxRequestHandlerProvider冲突问题
*
* @return /
*/
@Bean
@SuppressWarnings({"unchecked", "all"})
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
List<T> filteredMappings =
mappings.stream().filter(mapping -> mapping.getPatternParser() == null).collect(Collectors.toList());
mappings.clear();
mappings.addAll(filteredMappings);
}
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
if (field != null) {
field.setAccessible(true);
try {
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to access handlerMappings field", e);
}
}
return Collections.emptyList();
}
};
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2021-2025 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,42 @@
/*
* Copyright 2021-2025 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,46 @@
/*
* Copyright 2021-2025 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,91 @@
/*
* Copyright 2021-2025 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.hutool.core.exceptions.ExceptionUtil;
import cn.odboy.framework.exception.BadRequestException;
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
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有不可知的异常
*/
@ExceptionHandler(Throwable.class)
public ResponseEntity<ApiError> handleException(Throwable e) {
// 打印堆栈信息
log.error(ExceptionUtil.stacktraceToString(e));
return buildResponseEntity(ApiError.error(e.getMessage()));
}
/**
* BadCredentialsException
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> badCredentialsException(BadCredentialsException e) {
// 打印堆栈信息
String message = "坏的凭证".equals(e.getMessage()) ? "用户名或密码不正确" : e.getMessage();
log.error(message);
return buildResponseEntity(ApiError.error(message));
}
/**
* 处理自定义异常
*/
@ExceptionHandler(value = BadRequestException.class)
public ResponseEntity<ApiError> badRequestException(BadRequestException e) {
// 打印堆栈信息
log.error(ExceptionUtil.stacktraceToString(e));
return buildResponseEntity(ApiError.error(e.getStatus(), e.getMessage()));
}
/**
* 处理所有接口数据验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 打印堆栈信息
log.error(ExceptionUtil.stacktraceToString(e));
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
String message = objectError.getDefaultMessage();
if (objectError instanceof FieldError) {
message = ((FieldError) objectError).getField() + ": " + message;
}
return buildResponseEntity(ApiError.error(message));
}
/**
* 统一返回
*/
private ResponseEntity<ApiError> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, HttpStatus.valueOf(apiError.getStatus()));
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2021-2025 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.*;
/**
* 统计执行时长
*
* @author odboy
*/
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExecutionTimeStatisticsFace {
/**
* 接口描述
*/
String description();
/**
* 是否展示异常日志
*/
boolean showErrLog() default true;
}
@@ -0,0 +1,50 @@
/*
* Copyright 2021-2025 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 ExecutionTimeStatisticsFaceAspect {
@Around("@annotation(executionTimeStatisticsFace)")
public Object aroundFace(ProceedingJoinPoint joinPoint, ExecutionTimeStatisticsFace executionTimeStatisticsFace) throws Throwable {
TimeInterval timeInterval = new TimeInterval();
try {
Object proceed = joinPoint.proceed();
log.info("[{}] 接口执行成功,耗时: {} ms", executionTimeStatisticsFace.description(), timeInterval.intervalMs());
return proceed;
} catch (Exception e) {
if (executionTimeStatisticsFace.showErrLog()) {
log.error("[{}] 接口执行失败,耗时: {} ms", executionTimeStatisticsFace.description(), timeInterval.intervalMs(), e);
}
throw e;
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2021-2025 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.CsHealthCheckPointService;
import io.swagger.annotations.Api;
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")
@Api(tags = "系统:健康检查接口")
public class HealthCheckController {
@Autowired
private CsHealthCheckPointService healthCheckPointService;
/**
* 就绪检查
*/
@AnonymousGetMapping(value = "/readiness")
public ResponseEntity<?> doReadiness() {
return healthCheckPointService.doReadiness();
}
/**
* 存活检查
*/
@AnonymousGetMapping(value = "/liveness")
public ResponseEntity<?> doLiveness() {
return healthCheckPointService.doLiveness();
}
// /**
// * 访问首页提示
// */
// @AnonymousGetMapping("/")
// public String index() {
// return "success";
// }
}
@@ -0,0 +1,31 @@
/*
* Copyright 2021-2025 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 CsHealthCheckPointService {
/**
* 就绪检查
*/
ResponseEntity<?> doReadiness();
/**
* 存活检查
*/
ResponseEntity<?> doLiveness();
}
@@ -0,0 +1,87 @@
/*
* Copyright 2021-2025 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 CsMpQTypeEnum {
/**
* 相等
*/
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,46 @@
/*
* Copyright 2021-2025 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 CsMpQuery {
/**
* 基本对象的属性名
*/
String propName() default "";
/**
* 查询方式
*/
CsMpQTypeEnum type() default CsMpQTypeEnum.EQUAL;
/**
* 多字段模糊搜索, 仅支持String类型字段, 多个用逗号隔开, 如@MpQuery(blurry = "email,username")
*/
String blurry() default "";
}
@@ -0,0 +1,159 @@
/*
* Copyright 2021-2025 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.extern.slf4j.Slf4j;
/**
* @author /
*/
@Slf4j
@SuppressWarnings({"unchecked", "all"})
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 String feature;
private String desc;
private String driver;
private String keywordPrefix;
private String keywordSuffix;
private String aliasPrefix;
private String aliasSuffix;
private static final String JDBC_URL_PREFIX = "jdbc:";
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<?> aClass = Class.forName(dataTypeEnum.getDriver());
if (null == aClass) {
throw new BadRequestException("Unable to get driver instance for jdbcUrl: " + jdbcUrl);
}
} catch (ClassNotFoundException e) {
throw new BadRequestException("Unable to get driver instance: " + jdbcUrl);
}
return dataTypeEnum;
}
}
return null;
}
public String getFeature() {
return feature;
}
public String getDesc() {
return desc;
}
public String getDriver() {
return driver;
}
public String getKeywordPrefix() {
return keywordPrefix;
}
public String getKeywordSuffix() {
return keywordSuffix;
}
public String getAliasPrefix() {
return aliasPrefix;
}
public String getAliasSuffix() {
return aliasSuffix;
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2021-2025 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-2025 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 org.apache.ibatis.session.Configuration;
import java.util.List;
/**
* 批量插入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,55 @@
/*
* Copyright 2021-2025 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,92 @@
/*
* Copyright 2021-2025 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.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@SuppressWarnings({"unchecked", "all"})
@ConditionalOnWebApplication
@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true", matchIfMissing = true)
public class RemoveDruidAdConfig {
/**
* 方法名: removeDruidAdFilterRegistrationBean 方法描述 除去页面底部的广告
*
* @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties
* @return org.springframework.boot.web.servlet.FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
final String filePath = "support/http/resources/js/common.js";
//创建filter进行过滤
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (httpRequest.getRequestURI().endsWith("js/common.js")) {
// 获取common.js
String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?druid_banner\"></a><br/>", "");
text = text.replaceAll("powered by.*?shrek.wang</a>", "");
httpResponse.setContentType("application/javascript");
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.getWriter().write(text);
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}
@@ -0,0 +1,86 @@
/*
* Copyright 2021-2025 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.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义任意对象查询工具
*
* @author odboy
* @date 2025-05-15
*/
public class CsMpAnyQUtil {
public static <T> T selectOne(BaseMapper<T> baseMapper, Object queryParams) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
return baseMapper.selectOne(queryWrapper);
}
public static <T, M> M selectOne(BaseMapper<T> baseMapper, Object queryParams, Class<M> mapperClazz) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
T mapperTarget = baseMapper.selectOne(queryWrapper);
if (mapperTarget == null) {
return null;
}
return BeanUtil.copyProperties(mapperTarget, mapperClazz);
}
public static <T> List<T> selectList(BaseMapper<T> baseMapper, Object queryParams) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
return baseMapper.selectList(queryWrapper);
}
public static <T, M> List<M> selectList(BaseMapper<T> baseMapper, Object queryParams, Class<M> mapperClazz) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
List<T> mapperTarget = baseMapper.selectList(queryWrapper);
if (mapperTarget == null) {
return new ArrayList<>();
}
return BeanUtil.copyToList(mapperTarget, mapperClazz);
}
public static <T> Long selectCount(BaseMapper<T> baseMapper, Object queryParams) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
return baseMapper.selectCount(queryWrapper);
}
public static <T> Page<T> selectPage(BaseMapper<T> baseMapper, Page<T> page, Object queryParams) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
return baseMapper.selectPage(page, queryWrapper);
}
public static <T, M> Page<M> selectPage(
BaseMapper<T> baseMapper,
Page<T> page,
Object queryParams,
Class<M> mapperClazz) {
QueryWrapper<T> queryWrapper = CsMpQUtil.build(queryParams);
Page<T> tPage = baseMapper.selectPage(page, queryWrapper);
Page<M> mapperTarget = new Page<>();
mapperTarget.setRecords(BeanUtil.copyToList(tPage.getRecords(), mapperClazz));
mapperTarget.setTotal(tPage.getTotal());
mapperTarget.setSize(tPage.getSize());
mapperTarget.setCurrent(tPage.getCurrent());
return mapperTarget;
}
}
@@ -0,0 +1,228 @@
/*
* Copyright 2021-2025 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.CsBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
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 lombok.Data;
import org.apache.ibatis.annotations.Mapper;
import java.sql.Types;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* 代码生成器入口
*
* @author odboy
* @date 2025-01-11
*/
@Data
public class CsMpCmdGenUtil {
// 项目根路径。生成结果 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;
/**
* 代码生成函数
*
* @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 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(CsBaseUserTimeTb.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 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));
}
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);
});
}
}
@@ -0,0 +1,231 @@
/*
* Copyright 2021-2025 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.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.base.CsBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.framework.mybatisplus.config.CsMpQuery;
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 lombok.extern.slf4j.Slf4j;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* 查询条件创建工具
*/
@Slf4j
public class CsMpQUtil {
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);
CsMpQuery q = field.getAnnotation(CsMpQuery.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(CsMpQuery q, Field field) {
String attributeName = StrUtil.isBlank(q.propName()) ? field.getName() : q.propName();
return StrUtil.toUnderlineCase(attributeName);
}
private static <R> void handleWrapper(
CsMpQuery 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 (CollectionUtil.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 (CollectionUtil.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().collect(Collectors.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;
}
@TableName("test_domain")
private static class TestDomain {
@NotNull(groups = CsBaseUserTimeTb.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;
}
public static void main(String[] args) {
QueryWrapper<TestDomain> query = new QueryWrapper<TestDomain>();
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());
}
}
@@ -0,0 +1,189 @@
/*
* Copyright 2021-2025 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.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.odboy.base.CsPageResult;
import cn.odboy.util.CsPageUtil;
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.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.springframework.data.domain.Pageable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 公共抽象Mapper接口类
*
* @author odboy
* @date 2022-04-03
*/
public interface CsMpMapper<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 ConcurrentHashMap<>(1);
List<E> list = this.selectList(wrapper);
if (CollUtil.isEmpty(list)) {
return new HashMap<>(3);
}
E tempObj = list.stream().findFirst().orElse(null);
if (tempObj == null) {
return new HashMap<>(3);
}
Field[] fields = ReflectUtil.getFields(tempObj.getClass());
for (E e : list) {
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;
}
}
String fieldKey = String.valueOf(ReflectUtil.getFieldValue(e, keyField));
Object fieldValue = ReflectUtil.getFieldValue(e, valueField);
if (fieldKey != null) {
result.put(fieldKey, fieldValue);
}
}
return result;
}
/**
* 条件查询列表, 并返回期望的对象
*
* @param wrapper LambdaQueryWrapper<E>
* @param clazz 期望的对象类型
* @return List<T>
*/
default <T> List<T> queryFeatureClazzList(LambdaQueryWrapper<E> wrapper, Class<T> clazz) {
List<E> list = this.selectList(wrapper);
if (CollUtil.isEmpty(list)) {
return new ArrayList<>();
}
return BeanUtil.copyToList(list, clazz);
}
/**
* 条件查询列表, 并返回期望的对象
*
* @param wrapper LambdaQueryWrapper<E>
* @param clazz 期望的对象类型
* @return List<T>
*/
default <T> List<T> queryFeatureClazzList(LambdaQueryChainWrapper<E> wrapper, Class<T> clazz) {
List<E> list = wrapper.list();
if (CollUtil.isEmpty(list)) {
return new ArrayList<>();
}
return BeanUtil.copyToList(list, clazz);
}
/**
* 条件查询分页列表, 并返回期望的对象
*
* @param pageable 分页参数
* @param wrapper LambdaQueryWrapper<E> wrapper
* @param clazz 期望的对象类型
* @return IPage<T>
*/
default <T> CsPageResult<T> queryFeatureClazzPage(Pageable pageable, LambdaQueryWrapper<E> wrapper, Class<T> clazz) {
int pageNumber = pageable.getPageNumber();
int pageSize = pageable.getPageSize();
pageNumber = pageNumber <= 0 ? 1 : pageNumber;
pageSize = pageSize <= 0 ? 10 : pageSize;
IPage<E> pageInfo = this.selectPage(new Page<>(pageNumber, pageSize), wrapper);
if (CollUtil.isEmpty(pageInfo.getRecords())) {
return new CsPageResult<>(new ArrayList<>(), pageInfo.getTotal());
}
return new CsPageResult<>(BeanUtil.copyToList(pageInfo.getRecords(), clazz), pageInfo.getTotal());
}
/**
* 条件查询分页列表, 并返回期望的对象
*
* @param pageable 分页参数
* @param wrapper LambdaQueryWrapper<E> wrapper
* @param clazz 期望的对象类型
* @return IPage<T>
*/
default <T> CsPageResult<T> queryFeatureClazzPage(
Pageable pageable,
LambdaQueryChainWrapper<E> wrapper,
Class<T> clazz) {
int pageNumber = pageable.getPageNumber();
int pageSize = pageable.getPageSize();
pageNumber = pageNumber <= 0 ? 1 : pageNumber;
pageSize = pageSize <= 0 ? 10 : pageSize;
IPage<E> pageInfo = wrapper.page(new Page<>(pageNumber, pageSize));
if (CollUtil.isEmpty(pageInfo.getRecords())) {
return CsPageUtil.emptyListData();
}
return new CsPageResult<>(BeanUtil.copyToList(pageInfo.getRecords(), clazz), pageInfo.getTotal());
}
/**
* 获取目标类
*
* @param wrapper LambdaQueryChainWrapper<E> wrapper
* @param clazz 期望的对象类型
* @return T
*/
default <T> T getFeatureClazz(LambdaQueryChainWrapper<E> wrapper, Class<T> clazz) {
E e = this.selectOne(wrapper);
if (e == null) {
return null;
}
return BeanUtil.copyProperties(e, clazz);
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2021-2025 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.CsPageResult;
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 CsMpService<T> extends IService<T> {
<G> int saveFeatureClazz(G resources);
<G> int saveFeatureClazzList(List<G> resources);
<G> int modifyFeatureClazzById(G resources);
<G> boolean modifyFeatureClazzListById(Collection<G> resources, int batchSize);
<G> G queryFeatureClazzById(Serializable id, Class<G> targetClazz);
T queryClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn);
<G> G queryFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn, Class<G> clazz);
<G> List<G> queryFeatureClazzListByIds(List<Serializable> ids, Class<G> targetClazz);
<Q> List<T> queryClazzListByArgs(Q criteria);
List<T> queryClazzListByArgs(LambdaQueryWrapper<T> wrapper);
<G, Q> List<G> queryFeatureClazzListByArgs(Q criteria, Class<G> targetClazz);
<G> List<G> queryFeatureClazzListByArgs(LambdaQueryWrapper<T> wrapper, Class<G> targetClazz);
CsPageResult<T> queryClazzPageByArgs(LambdaQueryWrapper<T> wrapper, IPage<T> pageable);
<G> CsPageResult<G> queryFeatureClazzPageByArgs(
LambdaQueryWrapper<T> wrapper,
IPage<T> pageable,
Class<G> targetClazz);
<G, Q> CsPageResult<G> queryFeatureClazzPageByArgs(Q criteria, IPage<T> pageable, Class<G> targetClazz);
<Q> CsPageResult<T> queryClazzPageByArgs(Q criteria, IPage<T> pageable);
int modifyClazzByArgs(LambdaQueryWrapper<T> wrapper, T entity);
int removeClazzByArgs(LambdaQueryWrapper<T> wrapper);
}
@@ -0,0 +1,259 @@
/*
* Copyright 2021-2025 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.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.odboy.base.CsPageResult;
import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.framework.mybatisplus.core.CsMpQUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.ibatis.binding.MapperMethod;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 公共抽象Mapper接口类
*
* @author odboy
* @date 2022-04-03
*/
public class CsMpServiceImpl<M extends CsMpMapper<T>, T> extends ServiceImpl<M, T> {
/**
* 快速插入
*
* @param resources /
*/
public <G> int saveFeatureClazz(G resources) {
return getBaseMapper().insert(BeanUtil.copyProperties(resources, this.getEntityClass()));
}
/**
* 快速批量插入
*
* @param resources /
*/
public <G> int saveFeatureClazzList(List<G> resources) {
return getBaseMapper().insertBatchSomeColumn(BeanUtil.copyToList(resources, this.getEntityClass()));
}
/**
* 快速更新
*
* @param resources /
*/
public <G> int modifyFeatureClazzById(G resources) {
return getBaseMapper().updateById(BeanUtil.copyProperties(resources, this.getEntityClass()));
}
/**
* 快速批量更新
*
* @param resources /
* @param batchSize 每批数量
*/
public <G> boolean modifyFeatureClazzListById(Collection<G> resources, int batchSize) {
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", BeanUtil.copyProperties(entity, this.getEntityClass()));
sqlSession.update(sqlStatement, param);
});
}
/**
* 通过id查询
*
* @param id /
* @param targetClazz 目标类型
*/
public <G> G queryFeatureClazzById(Serializable id, Class<G> targetClazz) {
T entity = this.getById(id);
return entity == null ? null : BeanUtil.copyProperties(entity, targetClazz);
}
/**
* 查一条数据
*
* @param wrapper 制造条件
*/
public T queryClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn) {
if (orderColumn == null) {
throw new BadRequestException("参数orderColumn(排序的列)必填");
}
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
wrapper.last("limit 1");
wrapper.orderByDesc(orderColumn);
List<T> ts = baseMapper.selectList(wrapper);
if (ts.isEmpty()) {
return null;
}
return ts.stream().findFirst().get();
}
public <G> G queryFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn, Class<G> clazz) {
if (orderColumn == null) {
throw new BadRequestException("参数orderColumn(排序的列)必填");
}
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
wrapper.last("limit 1");
wrapper.orderByDesc(orderColumn);
List<T> ts = baseMapper.selectList(wrapper);
if (ts.isEmpty()) {
return null;
}
return BeanUtil.copyProperties(ts.stream().findFirst().get(), clazz);
}
/**
* 通过id集合查询
*
* @param ids /
* @param targetClazz 目标类型
*/
public <G> List<G> queryFeatureClazzListByIds(List<Serializable> ids, Class<G> targetClazz) {
List<T> entitys = this.listByIds(ids);
return entitys == null || entitys.isEmpty() ? CollUtil.newArrayList() : BeanUtil.copyToList(entitys, targetClazz);
}
/**
* 查有关的数据
*
* @param criteria 动态条件
*/
public <Q> List<T> queryClazzListByArgs(Q criteria) {
QueryWrapper<T> predicate = CsMpQUtil.build(criteria);
return baseMapper.selectList(predicate);
}
/**
* 查有关的数据
*
* @param wrapper 制造条件
*/
public List<T> queryClazzListByArgs(LambdaQueryWrapper<T> wrapper) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
return baseMapper.selectList(wrapper);
}
/**
* 查有关的数据并转成目标类型的数据
*
* @param criteria 动态条件
* @param targetClazz 目标类型
*/
public <G, Q> List<G> queryFeatureClazzListByArgs(Q criteria, Class<G> targetClazz) {
QueryWrapper<T> predicate = CsMpQUtil.build(criteria);
List<T> data = baseMapper.selectList(predicate);
return BeanUtil.copyToList(data, targetClazz);
}
/**
* 查有关的数据并转换为目标类型
*
* @param wrapper 制造条件
* @param targetClazz 目标类型
*/
public <G> List<G> queryFeatureClazzListByArgs(LambdaQueryWrapper<T> wrapper, Class<G> targetClazz) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
return BeanUtil.copyToList(baseMapper.selectList(wrapper), targetClazz);
}
/**
* 查有关的分页数据
*
* @param wrapper 制造条件
* @param pageable 分页参数
*/
public CsPageResult<T> queryClazzPageByArgs(LambdaQueryWrapper<T> wrapper, IPage<T> pageable) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
IPage<T> originPageData = baseMapper.selectPage(pageable, wrapper);
return new CsPageResult<>(originPageData.getRecords(), originPageData.getTotal());
}
/**
* 查有关的分页数据
*
* @param wrapper 制造条件
* @param pageable 分页参数
* @param targetClazz 目标类型
*/
public <G> CsPageResult<G> queryFeatureClazzPageByArgs(
LambdaQueryWrapper<T> wrapper,
IPage<T> pageable,
Class<G> targetClazz) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
IPage<T> originPageData = baseMapper.selectPage(pageable, wrapper);
return new CsPageResult<>(BeanUtil.copyToList(originPageData.getRecords(), targetClazz), originPageData.getTotal());
}
public <G, Q> CsPageResult<G> queryFeatureClazzPageByArgs(Q criteria, IPage<T> pageable, Class<G> targetClazz) {
QueryWrapper<T> wrapper = CsMpQUtil.build(criteria);
IPage<T> originPageData = baseMapper.selectPage(pageable, wrapper);
return new CsPageResult<>(BeanUtil.copyToList(originPageData.getRecords(), targetClazz), originPageData.getTotal());
}
public <Q> CsPageResult<T> queryClazzPageByArgs(Q criteria, IPage<T> pageable) {
QueryWrapper<T> wrapper = CsMpQUtil.build(criteria);
IPage<T> originPageData = baseMapper.selectPage(pageable, wrapper);
return new CsPageResult<>(originPageData.getRecords(), originPageData.getTotal());
}
/**
* 条件更新
*
* @param wrapper 制造条件
* @param entity 更新内容
*/
public int modifyClazzByArgs(LambdaQueryWrapper<T> wrapper, T entity) {
if (wrapper == null) {
throw new BadRequestException("参数wrapper必填");
}
return baseMapper.update(entity, wrapper);
}
/**
* 条件删除
*
* @param wrapper 制造条件
*/
public int removeClazzByArgs(LambdaQueryWrapper<T> wrapper) {
if (wrapper == null) {
throw new BadRequestException("参数wrapper必填");
}
return baseMapper.delete(wrapper);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2025 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.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 应用配置
*
* @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;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CaptchaModel extends CsObject {
/**
* 验证码有效时间, 单位: 秒
*/
private Long expireTime;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
/**
* 密码加密传输, 前端公钥加密,后端私钥解密
*/
@Getter
@Setter
public class ContentRsaEncodeSettingModel extends CsObject {
private String privateKey;
}
@@ -0,0 +1,48 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
/**
* Jwt参数配置
*/
@Getter
@Setter
public class JwtAuthSettingModel extends CsObject {
/**
* 必须使用最少88位的Base64对该令牌进行编码
*/
private String base64Secret;
/**
* 令牌过期时间 此处单位/毫秒
*/
private Long tokenValidityInSeconds;
/**
* token 续期检查
*/
private Long detect;
/**
* 续期时间
*/
private Long renew;
}
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OSSConfigModel extends CsObject {
/**
* 服务地址
*/
private String endpoint;
/**
* 存储桶名称
*/
private String bucketName;
/**
* Access Key
*/
private String accessKey;
/**
* Secret Key
*/
private String secretKey;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2025 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-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
/**
* @author odboy
* @date 2025-04-13
*/
@Getter
@Setter
public class SwaggerApiDocSettingModel extends CsObject {
private Boolean enabled;
}
@@ -0,0 +1,48 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
/**
* 线程池 配置
*
* @author odboy
* @date 2025-04-13
*/
@Getter
@Setter
public class ThreadPoolSettingModel extends CsObject {
/**
* 核心线程池大小
*/
private int corePoolSize;
/**
* 最大线程数
*/
private int maxPoolSize;
/**
* 活跃时间
*/
private int keepAliveSeconds;
/**
* 队列容量
*/
private int queueCapacity;
}
@@ -0,0 +1,132 @@
/*
* Copyright 2021-2025 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.CsObject;
import cn.odboy.constant.CaptchaCodeEnum;
import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.util.CsStringUtil;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import lombok.Getter;
import lombok.Setter;
import java.awt.*;
/**
* 验证码配置
*
* @author odboy
*/
@Getter
@Setter
public class UserLoginCaptchaSettingModel extends CsObject {
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-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
/**
* 登录配置
*
* @author odboy
*/
@Getter
@Setter
public class UserLoginSettingModel extends CsObject {
/**
* 账号单用户 登录
*/
private boolean single = false;
private UserLoginCaptchaSettingModel captchaSetting;
}
@@ -0,0 +1,63 @@
/*
* Copyright 2021-2025 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 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.lang.NonNull;
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,57 @@
/*
* Copyright 2021-2025 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.CsSpringBeanHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
/**
* 执行定时任务
*/
@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 = CsSpringBeanHolder.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
@SuppressWarnings({"unchecked", "all"})
public Object call() throws Exception {
ReflectionUtils.makeAccessible(method);
if (StringUtils.isNotBlank(params)) {
return method.invoke(target, params);
} else {
return method.invoke(target);
}
}
}
@@ -0,0 +1,843 @@
/*
* Copyright 2021-2025 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 com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
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.*;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
@SuppressWarnings({"unchecked", "all"})
public class CsRedisHelper {
private static final Logger log = LoggerFactory.getLogger(CsRedisHelper.class);
private RedisTemplate<Object, Object> redisTemplate;
public CsRedisHelper(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 /
*/
public List<String> scan(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> 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 /
*/
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 redisTemplate.hasKey(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... keys) {
if (keys != null && keys.length > 0) {
if (keys.length == 1) {
boolean result = redisTemplate.delete(keys[0]);
log.debug("--------------------------------------------");
log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(", 结果:").append(result).toString());
log.debug("--------------------------------------------");
} else {
Set<Object> keySet = new HashSet<>();
for (String key : keys) {
if (redisTemplate.hasKey(key)) {
keySet.add(key);
}
}
long count = redisTemplate.delete(keySet);
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keySet.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
}
/**
* 批量模糊删除key
*
* @param pattern
*/
public void scanDel(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
try (Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(
(RedisCallback<Cursor<byte[]>>) connection -> (Cursor<byte[]>) new ConvertingCursor<>(
connection.scan(options),
redisTemplate.getKeySerializer()::deserialize))) {
while (cursor.hasNext()) {
redisTemplate.delete(cursor.next());
}
}
}
// ============================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 类型 by
if (!clazz.isInstance(value)) {
return JSON.parseObject(value.toString(), clazz);
} else if (clazz.isInstance(value)) {
return clazz.cast(value);
} else {
return null;
}
}
/**
* 普通缓存获取
*
* @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<?> list = (List<?>) value;
// 检查每个元素是否为指定类型
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 list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys));
List 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, time, TimeUnit.SECONDS);
} 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) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
} 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 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 {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 将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;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} 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;
} 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(new StringBuffer(prefix).append(id).toString()));
}
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 {
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 timeout 过期时间
* @param unit 时间单位
* @return 如果设置成功返回true,否则返回false
*/
public Boolean setIfAbsent(String key, Object value, long timeout, TimeUnit unit) {
try {
String stringValue = ObjUtil.toString(value);
// 使用Redis的SETNX命令实现原子性操作
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, stringValue, timeout, unit);
return success != null && success;
} catch (Exception e) {
log.error("Redis setIfAbsent操作失败, key: {}", key, e);
return false;
}
}
}
@@ -0,0 +1,202 @@
/*
* Copyright 2021-2025 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 com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONFactory;
import com.alibaba.fastjson2.JSONWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.MurmurHash3;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfiguration extends CachingConfigurerSupport {
/**
* 自动识别json对象白名单配置(仅允许解析的包名, 范围越小越安全)<br/> 未配置可能导致, 登录失败, 反复登录等问题
*/
private static final String[] WHITELIST_STR =
{
"org.springframework",
"cn.odboy.system.dal.dataobject",
"cn.odboy.system.dal.model",
"cn.odboy.task.dal.dataobject",
"cn.odboy.task.dal.model",
};
/**
* 设置 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 : WHITELIST_STR) {
JSONFactory.getDefaultObjectReaderProvider().addAutoTypeAccept(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
@Override
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
@SuppressWarnings({"unchecked", "all"})
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
// 处理缓存读取错误
log.error("Cache Get Error: {}", exception.getMessage());
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
// 处理缓存写入错误
log.error("Cache Put Error: {}", exception.getMessage());
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
// 处理缓存删除错误
log.error("Cache Evict Error: {}", exception.getMessage());
}
@Override
public void handleCacheClearError(RuntimeException exception, 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,101 @@
/*
* Copyright 2021-2025 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 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;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 创建自定义的线程池
*/
@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-2025 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 org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
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 java.util.stream.Collectors;
@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);
}
}).collect(Collectors.toList());
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2025 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 org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@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,123 @@
/*
* Copyright 2021-2025 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.spring.http.converter.FastJsonHttpMessageConverter;
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.HttpMessageConverter;
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;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 跨越、静态资源映射、Http消息转换
*
* @author odboy
* @date 2025-03-21
*/
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
@Autowired
private DateStringArraysToListDateConverter dateStringArraysToListDateConverter;
@Autowired
private DateStringToDateConverter dateStringToDateConverter;
@Autowired
private CsFileLocalUploadHelper 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 = 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);
fastJsonConverter.setFastJsonConfig(config);
fastJsonConverter.setSupportedMediaTypes(supportMediaTypeList);
fastJsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
// 将 FastJsonHttpMessageConverter 添加到列表末尾
converters.add(fastJsonConverter);
}
/**
* 自定义时间转换器
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(dateStringArraysToListDateConverter);
registry.addConverter(dateStringToDateConverter);
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2021-2025 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.constant.SystemConst;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 简化文件上传路径配置
*
* @author odboy
*/
@Component
public class CsFileLocalUploadHelper {
/**
* 单位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/odboy/${appName}/file/";
private static final String PATH_LINUX = "/home/${appName}/file/";
@Value("${spring.application.name}")
private String appName;
public String getPath() {
String os = System.getProperty("os.name");
if (os.toLowerCase().startsWith(SystemConst.WIN)) {
return PATH_WINDOWS.replace("${appName}", appName);
} else if (os.toLowerCase().startsWith(SystemConst.MAC)) {
return PATH_MAC.replace("${appName}", appName);
}
return PATH_LINUX.replace("${appName}", appName);
}
public int getAvatarMaxSize() {
return AVATAR_MAX_SIZE;
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2025 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;
@Configuration
public class WsConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2025 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 io.undertow.server.DefaultByteBufferPool;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
/**
* 解决启动io.undertow.websockets.jsr UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used的警告
*
* @date 2024-06-07
*/
@Component
public class WsFactory implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Override
public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
deploymentInfo.addServletContextAttribute(
"io.undertow.websockets.jsr.WebSocketDeploymentInfo",
webSocketDeploymentInfo);
});
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2021-2025 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 lombok.extern.slf4j.Slf4j;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket客户端管理
*
* @author odboy
* @date 2025-07-24
*/
@Slf4j
public class CsWsClientManager {
/**
* concurrent包的线程安全Map, 用来存放每个客户端对应的MyWebSocket对象。(分布式必出问题)
*/
private static final Map<String, CsWsServer> clientMap = new ConcurrentHashMap<>();
public static void addClient(String sid, CsWsServer csWsServer) {
// 如果存在就先删除一个, 防止重复推送消息
removeClient(sid);
clientMap.put(sid, csWsServer);
}
public static void removeClient(String sid) {
CsWsServer 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);
}
// log.info("停止任务, sid={}", sid);
wsServer.stopTask(sid);
// log.info("移除客户端, sid={}", sid);
clientMap.remove(sid);
}
}
public static Collection<CsWsServer> getAllClient() {
return clientMap.values();
}
public static CsWsServer getClientBySid(String sid) {
return clientMap.get(sid);
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CsWsMessage extends CsObject {
private String bizCode;
private String data;
public CsWsMessage(String bizCode, String data) {
this.bizCode = bizCode;
this.data = data;
}
}
@@ -0,0 +1,175 @@
/*
* Copyright 2021-2025 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 com.alibaba.fastjson2.JSON;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint("/websocket/{sid}")
@Getter
public class CsWsServer {
private static final Map<String, Thread> taskThreadMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话, 需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收sid
*/
private String sid = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
// {username}#{bizCode}#{contextParams}
this.sid = sid;
CsWsClientManager.addClient(sid, this);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
CsWsMessage wsMessage = JSON.parseObject(message, CsWsMessage.class);
String bizCode = wsMessage.getBizCode();
Object data = wsMessage.getData();
log.info("收到来 sid={} 的信息: message={}, bizCode={}, data={}", sid, message, bizCode, JSON.toJSONString(data));
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
CsWsClientManager.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 {
CsWsClientManager.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(CsWsMessage message, @PathParam("sid") String sid) throws IOException {
String body = JSON.toJSONString(message);
// log.info("推送消息到{}, 推送内容:{}", sid, message);
try {
if (sid == null) {
sendToAll(body);
} else {
CsWsServer wsClient = CsWsClientManager.getClientBySid(sid);
if (wsClient != null) {
wsClient.innerSendMessage(body);
}
}
} catch (Exception ignored) {
}
}
/**
* 群发消息
*/
public void sendToAll(String message) {
for (CsWsServer item : CsWsClientManager.getAllClient()) {
try {
item.innerSendMessage(message);
} catch (IOException e) {
log.error("发送消息给 sid={} 失败", item.sid, e);
CsWsClientManager.removeClient(item.sid);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CsWsServer that = (CsWsServer) o;
return Objects.equals(session, that.session) && Objects.equals(sid, that.sid);
}
@Override
public int hashCode() {
return Objects.hash(session, sid);
}
public Thread getTaskThread(String sid) {
Thread thread = taskThreadMap.get(sid);
if (thread == null) {
return null;
}
if (thread.isInterrupted()) {
taskThreadMap.remove(sid);
return null;
}
return thread;
}
public void restartTask(Runnable runnable) {
stopTask(this.sid);
Thread thread = new Thread(runnable);
thread.start();
taskThreadMap.put(this.sid, thread);
}
public void stopTask(String sid) {
Thread thread = getTaskThread(this.sid);
if (thread != null) {
thread.stop();
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2021-2025 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.CsObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class WsSidVo extends CsObject {
private String username;
private String bizCode;
private String param;
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2025 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-2025 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.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
/**
* 匿名标记工具
*/
@UtilityClass
public class CsAnonTagUtil {
/**
* 获取匿名标记的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, java.util.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());
PatternsRequestCondition patternsCondition = infoEntry.getKey().getPatternsCondition();
if (patternsCondition != null) {
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(patternsCondition.getPatterns());
break;
case POST:
post.addAll(patternsCondition.getPatterns());
break;
case PUT:
put.addAll(patternsCondition.getPatterns());
break;
case PATCH:
patch.addAll(patternsCondition.getPatterns());
break;
case DELETE:
delete.addAll(patternsCondition.getPatterns());
break;
default:
all.addAll(patternsCondition.getPatterns());
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,50 @@
/*
* Copyright 2021-2025 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 CsBase64SecretUtil {
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,154 @@
/*
* Copyright 2021-2025 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;
/**
* 计算类
*/
@UtilityClass
public final class CsBigDecimalUtil {
/**
* 将对象转换为 BigDecimal
*
* @param obj 输入对象
* @return 转换后的 BigDecimal
*/
private static BigDecimal toBigDecimal(Object obj) {
if (obj instanceof BigDecimal) {
return (BigDecimal) obj;
} else if (obj instanceof Long) {
return BigDecimal.valueOf((Long) obj);
} else if (obj instanceof Integer) {
return BigDecimal.valueOf((Integer) obj);
} else if (obj instanceof Double) {
return new BigDecimal(String.valueOf(obj));
} else {
throw new IllegalArgumentException("Unsupported type");
}
}
/**
* 加法
*
* @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).setScale(2, RoundingMode.HALF_UP);
}
/**
* 减法
*
* @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).setScale(2, RoundingMode.HALF_UP);
}
/**
* 乘法
*
* @param a 乘数
* @param b 乘数
* @return 两个乘数的积, 保留两位小数
*/
public static BigDecimal multiply(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.multiply(bdB).setScale(2, RoundingMode.HALF_UP);
}
/**
* 除法
*
* @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, 2, RoundingMode.HALF_UP);
}
/**
* 除法
*
* @param a 被除数
* @param b 除数
* @param scale 保留小数位数
* @return 两数的商,保留两位小数
*/
public static BigDecimal divide(Object a, Object b, int scale) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.divide(bdB, scale, RoundingMode.HALF_UP);
}
/**
* 分转元
*
* @param obj 分的金额
* @return 转换后的元,保留两位小数
*/
public static BigDecimal centsToYuan(Object obj) {
BigDecimal cents = toBigDecimal(obj);
return cents.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
}
/**
* 元转分
*
* @param obj 元的金额
* @return 转换后的分
*/
public static Long yuanToCents(Object obj) {
BigDecimal yuan = toBigDecimal(obj);
return yuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
}
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));
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2021-2025 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 javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 浏览器 相关
*/
@Slf4j
@UtilityClass
public final class CsBrowserUtil {
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,39 @@
/*
* Copyright 2021-2025 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 CsClassUtil {
@SuppressWarnings({"unchecked", "all"})
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,49 @@
/*
* Copyright 2021-2025 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 CsCloseUtil {
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,224 @@
/*
* Copyright 2021-2025 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.CsSelectOptionVo;
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.*;
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 CsCollUtil 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 (CsCollUtil.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 (CsCollUtil.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) {
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<CsSelectOptionVo> testData = new ArrayList<>();
CsSelectOptionVo selectOption1 = CsSelectOptionVo.builder().label("苹果").value("apple").build();
CsSelectOptionVo selectOption2 = CsSelectOptionVo.builder().label("苹果").value("apple").build();
CsSelectOptionVo selectOption3 = CsSelectOptionVo.builder().label("香蕉").value("banana").build();
CsSelectOptionVo selectOption4 = CsSelectOptionVo.builder().label("猫").value("cat").build();
testData.add(selectOption1);
testData.add(selectOption2);
testData.add(selectOption3);
testData.add(selectOption4);
List<CsSelectOptionVo> selectOptions1 = CsCollUtil.streamAscSort(testData, CsSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions1));
List<CsSelectOptionVo> selectOptions2 = CsCollUtil.streamDescSort(testData, CsSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions2));
List<CsSelectOptionVo> selectOptions3 = CsCollUtil.streamDistinct(testData, CsSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions3));
Map<String, String> selectMap1 =
CsCollUtil.streamToMap(testData, CsSelectOptionVo::getValue, CsSelectOptionVo::getLabel);
System.out.println(JSON.toJSONString(selectMap1));
// 这只是一个示范
BigDecimal total1 = CsCollUtil.streamBigDecimalAdd(testData, "value");
System.out.println(total1);
}
}
@@ -0,0 +1,150 @@
/*
* Copyright 2021-2025 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.DateUtil;
import lombok.experimental.UtilityClass;
import java.util.Date;
/**
* 时间相关工具
*
* @author odboy
* @date 2025-10-01
*/
@UtilityClass
public class CsDateUtil {
/**
* 获取当前时间毫秒数
*
* @return /
*/
public static String getNowDateTimeMsStr() {
return DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN);
}
/**
* 获取当前时间,但不包括毫秒数
*
* @return /
*/
public static String getNowDateTimeStr() {
return DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
}
/**
* 获取当前时间,但不包括毫秒数
*
* @return /
*/
public static String getNowDateStr() {
return DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN);
}
/**
* 格式化到毫秒
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 格式化后的时间,X天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();
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2021-2025 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 CsDesEncryptUtil {
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,206 @@
/*
* Copyright 2021-2025 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
*/
public final class CsDistributedIdUtil {
/**
* 起始毫秒时间戳, 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);
/**
* 单例实例持有者类
*/
private static class InstanceHolder {
private static final CsDistributedIdUtil INSTANCE = new CsDistributedIdUtil(20, 12, 5, 5, 12);
}
/**
* 构造函数, 初始化数据中心 ID 和机器 ID
*
* @param dataCenterId 数据中心 ID
* @param workerId 机器 ID
* @param dataCenterIdBits 数据中心 ID 所占位数
* @param workerIdBits 机器 ID 所占位数
* @param sequenceBits 序列号所占位数
*/
public CsDistributedIdUtil(
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 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;
}
/**
* 获取单例实例的下一个分布式 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 + " - " + CsDistributedIdUtil.fastNextId());
}
}).start();
}
}
}
@@ -0,0 +1,398 @@
/*
* Copyright 2021-2025 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.IoUtil;
import cn.hutool.core.util.IdUtil;
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 javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
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.*;
/**
* File工具类, 扩展 hutool 工具包
*/
@Slf4j
@UtilityClass
public final class CsFileUtil extends cn.hutool.core.io.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;
/**
* 格式化小数
*/
private static final DecimalFormat DF = new DecimalFormat("0.00");
/**
* 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 = DF.format(size / (float) GB) + " GB";
} else if (size / MB >= 1) {
//如果当前Byte的值大于等于1MB
resultSize = DF.format(size / (float) MB) + " MB";
} else if (size / KB >= 1) {
//如果当前Byte的值大于等于1KB
resultSize = DF.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 {
CsCloseUtil.close(os);
CsCloseUtil.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
*/
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 {
CsCloseUtil.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));
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2021-2025 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.CsSpringBeanHolder;
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 CsIPUtil {
/**
* 注入bean
*/
private final static Ip2regionSearcher IP_SEARCHER = CsSpringBeanHolder.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,50 @@
/*
* Copyright 2021-2025 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 CsJsonUtil {
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(CsJsonUtil.toMap(mapStr, String.class, String.class));
String listStr = "[\"value1\", \"value2\"]";
System.err.println(CsJsonUtil.toList(listStr, String.class));
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2021-2025 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.CsBaseUserTimeTb;
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 CsListUtil {
/**
* 根据对象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(monitorItem -> monitorItem.iterator().next())
.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<CsBaseUserTimeTb> entities = new ArrayList<>();
int total = 50;
for (int i = 0; i < total; i++) {
CsBaseUserTimeTb entity = new CsBaseUserTimeTb();
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<CsBaseUserTimeTb> entities1 = CsListUtil.distinctByArgs(entities, CsBaseUserTimeTb::getCreateBy);
System.err.println(entities1);
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2025 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 CsLogFmtUtil {
/**
* 替换字符串中的占位符 {} 为传入的参数
*
* @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,91 @@
/*
* Copyright 2021-2025 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.CsPageResult;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 分页工具
*
* @author odboy
*/
@UtilityClass
public final class CsPageUtil 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> CsPageResult<T> toPage(IPage<T> page) {
return new CsPageResult<>(page.getRecords(), page.getTotal());
}
/**
* 自定义分页
*/
public static <T> CsPageResult<T> toPage(List<T> list) {
return new CsPageResult<>(list, list.size());
}
/**
* 返回空数据
*/
public static <T> CsPageResult<T> emptyListData() {
return new CsPageResult<>(new ArrayList<>(), 0);
}
/**
* 返回空数据
*/
public static <T> CsPageResult<T> emptyData() {
return new CsPageResult<>(null, 0);
}
/**
* 自定义分页
*/
public static <T> CsPageResult<T> toPage(List<T> list, long totalElements) {
return new CsPageResult<>(list, totalElements);
}
public static <T> Page<T> getCount(BaseMapper<T> baseMapper) {
return new Page<>(1, baseMapper.selectCount(null));
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2021-2025 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 CsResourceTemplateUtil {
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,73 @@
/*
* Copyright 2021-2025 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 CsReturnValueHandleUtil {
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);
}
}
}
@@ -0,0 +1,214 @@
/*
* Copyright 2021-2025 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 org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* Rsa 工具类, 公钥私钥生成, 加解密
*/
@UtilityClass
public final class CsRsaEncryptUtil {
private static final String SRC = "123456";
public static void main(String[] args) throws Exception {
System.out.println("\n");
RsaKeyPair keyPair = generateKeyPair();
System.out.println("公钥:" + keyPair.getPublicKey());
System.out.println("私钥:" + keyPair.getPrivateKey());
System.out.println("\n");
test1(keyPair);
System.out.println("\n");
test2(keyPair);
System.out.println("\n");
}
/**
* 公钥加密私钥解密
*/
private static void test1(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 公钥加密私钥解密开始 *****************");
String text1 = encryptByPublicKey(keyPair.getPublicKey(), CsRsaEncryptUtil.SRC);
String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1);
System.out.println("加密前:" + CsRsaEncryptUtil.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (CsRsaEncryptUtil.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致, 解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 公钥加密私钥解密结束 *****************");
}
/**
* 私钥加密公钥解密
*
* @throws Exception /
*/
private static void test2(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 私钥加密公钥解密开始 *****************");
String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), CsRsaEncryptUtil.SRC);
String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1);
System.out.println("加密前:" + CsRsaEncryptUtil.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (CsRsaEncryptUtil.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 私钥加密公钥解密结束 *****************");
}
/**
* 公钥解密
*
* @param publicKeyText 公钥
* @param text 待解密的信息
* @return /
* @throws Exception /
*/
public static String decryptByPublicKey(String publicKeyText, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text));
return new String(result);
}
/**
* 私钥加密
*
* @param privateKeyText 私钥
* @param text 待加密的信息
* @return /
* @throws Exception /
*/
public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param privateKeyText 私钥
* @param text 待解密的文本
* @return /
* @throws Exception /
*/
public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text));
return new String(result);
}
/**
* 公钥加密
*
* @param publicKeyText 公钥
* @param text 待加密的文本
* @return /
*/
public static String encryptByPublicKey(String publicKeyText, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes());
return Base64.encodeBase64String(result);
}
private static byte[] doLongerCipherFinal(int opMode, Cipher cipher, byte[] source) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (opMode == Cipher.DECRYPT_MODE) {
out.write(cipher.doFinal(source));
} else {
int offset = 0;
int totalSize = source.length;
while (totalSize - offset > 0) {
int size = Math.min(cipher.getOutputSize(0) - 11, totalSize - offset);
out.write(cipher.doFinal(source, offset, size));
offset += size;
}
}
out.close();
return out.toByteArray();
}
/**
* 构建RSA密钥对
*
* @return /
* @throws NoSuchAlgorithmException /
*/
public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RsaKeyPair(publicKeyString, privateKeyString);
}
/**
* RSA密钥对对象
*/
public static class RsaKeyPair {
private final String publicKey;
private final String privateKey;
public RsaKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
}
@@ -0,0 +1,128 @@
/*
* Copyright 2021-2025 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 lombok.extern.slf4j.Slf4j;
import java.util.Calendar;
import java.util.Date;
/**
* 字符串工具类, 继承org.apache.commons.lang3.StringUtils类
*/
@Slf4j
@UtilityClass
public final class CsStringUtil {
private static final char SEPARATOR = '_';
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCamelCase(String s) {
if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCapitalizeCamelCase(String s) {
if (s == null) {
return null;
}
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toUnderScoreCase(String s) {
if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
/**
* 获得当天是周几
*/
public static String getWeekDay() {
// String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
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];
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2021-2025 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 oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
/**
* 系统层相关
*
* @author odboy
* @date 2025-08-11
*/
public class CsSystemUtil {
public static int getCpuCount() {
SystemInfo systemInfo = new SystemInfo();
CentralProcessor processor = systemInfo.getHardware().getProcessor();
return processor.getPhysicalProcessorCount();
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2021-2025 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 lombok.experimental.UtilityClass;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Bean验证工具
*
* @author odboy
* @date 2025-01-15
*/
@UtilityClass
public final class CsValidUtil {
private static final Validator VALIDATOR;
static {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
VALIDATOR = factory.getValidator();
}
}
public static <T> void validate(T object) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validate(object, Default.class);
if (!violations.isEmpty()) {
throw new BadRequestException(violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(",")));
}
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cutejava</artifactId>
<groupId>cn.odboy</groupId>
<version>1.4.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cutejava-module-system</artifactId>
<name>核心模块</name>
<dependencies>
<dependency>
<groupId>cn.odboy</groupId>
<artifactId>cutejava-framework</artifactId>
<version>1.4.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.17</version>
</dependency>
</dependencies>
</project>

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