初始化

Spring Initializr

Spring Initializr
Maven + Java 21 + 4.0.0
Lombok + Spring Web +Spring Boot DevTools + Spring Configuration Processor + MySQL Driver + MyBatis Framework + Spring Data Elasticsearch

Spring AOP

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>4.0.0-M2</version>
</dependency>

MyBatis-plus

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.15</version>
</dependency>
1
2
3
4
5
6
<!-- mybatis-plus 分页插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.15</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 删除这个 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>

<!-- 删除这个(测试用的也不需要) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>

commons-lang3

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>

hutool

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.42</version>
</dependency>

腾讯云cos

1
2
3
4
5
6
<!--https://search.maven.org/artifact/com.qcloud/cos_api-->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.259</version>
</dependency>

微信公众号

1
2
3
4
5
6
<!-- https://github.com/binarywang/WxJava -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.7.0</version>
</dependency>
1
2
3
4
5
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.7.0</version>
</dependency>

freemarker 加载模板、填充数据并生成输出文本

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.34</version>
</dependency>

Elasticsearch

1
2
3
4
5
6
<!-- https://www.elastic.co/docs/reference/elasticsearch/clients/java/getting-started -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.2.2</version>
</dependency>
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>4.0.0</version>
</dependency>

SpringDoc

springdoc
springdoc-openapi-starter-webmvc-ui

1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.0</version>
</dependency>

Swagger UI 界面 http://localhost:8101/api/swagger-ui.html
OpenAPI JSON 文档 http://localhost:8101/api/v3/api-docs

fast excel

fast excel
Install-Package FastExcel

数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-- 创建库
create database if not exists omni_db;

-- 切换库
use omni_db;

-- 用户表
create table if not exists user
(
id bigint auto_increment comment 'id' primary key,
user_account varchar(256) not null comment '账号',
user_password varchar(512) not null comment '密码',
union_id varchar(256) null comment '微信开放平台id',
mpOpen_id varchar(256) null comment '公众号openId',
user_name varchar(256) null comment '用户昵称',
user_avatar varchar(1024) null comment '用户头像',
user_profile varchar(512) null comment '用户简介',
user_role varchar(256) default 'user' not null comment '用户角色:user/admin/ban',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
is_delete tinyint default 0 not null comment '是否删除',
index idx_unionId (union_id)
) comment '用户' collate = utf8mb4_unicode_ci;

-- 帖子表
create table if not exists post
(
id bigint auto_increment comment 'id' primary key,
title varchar(512) null comment '标题',
content text null comment '内容',
tags varchar(1024) null comment '标签列表(json 数组)',
thumb_num int default 0 not null comment '点赞数',
favour_num int default 0 not null comment '收藏数',
user_id bigint not null comment '创建用户 id',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
is_delete tinyint default 0 not null comment '是否删除',
index idx_userId (user_id)
) comment '帖子' collate = utf8mb4_unicode_ci;

-- 帖子点赞表(硬删除)
create table if not exists post_thumb
(
id bigint auto_increment comment 'id' primary key,
post_id bigint not null comment '帖子 id',
user_id bigint not null comment '创建用户 id',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
index idx_postId (post_id),
index idx_userId (user_id)
) comment '帖子点赞';

-- 帖子收藏表(硬删除)
create table if not exists post_favour
(
id bigint auto_increment comment 'id' primary key,
post_id bigint not null comment '帖子 id',
user_id bigint not null comment '创建用户 id',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
index idx_postId (post_id),
index idx_userId (user_id)
) comment '帖子收藏';

主程序

1
2
3
4
5
6
7
8
9
10
11
// todo 如需开启 Redis,须移除 exclude 中的内容 RedisAutoConfiguration.class
@SpringBootApplication
@MapperScan("fun.wgfun.Omni.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class OmniApplication {

public static void main(String[] args) {
SpringApplication.run(OmniApplication.class, args);
}
}

@EnableScheduling:启用 Spring 定时任务
告诉 Spring 容器:“请开启对 @Scheduled 注解的支持”,从而允许在任意被 Spring 管理的 Bean 中,通过 @Scheduled 注解定义定时执行的方法。
@Scheduled(fixedRate = 5000)5s执行一次

@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
用于开启 Spring AOP 的核心注解,特别针对 @Aspect 切面 和 @Transactional、@Cacheable 等代理型注解。
proxyTargetClass = true 强制 Spring 使用 CGLIB 代理,而不是 JDK 动态代理
exposeProxy = true 将当前代理对象暴露到 ThreadLocal 中,允许在目标方法内部通过 AopContext.currentProxy() 获取代理对象。

权限效验注解 AuthCheck

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
/**
* 必须有角色 role:user
* @return ""
*/
String mustRole() default "";
}

AOP 切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 权限校验 AOP
*/
@Aspect
@Component
public class AuthInterceptor {

// 使用userService
@Resource
private UserService userService;

/**
* 执行拦截
*
* @param joinPoint
* @param authCheck
* @return
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
User loginUser = userService.getLoginUser(request);
UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
// 不需要权限,放行
if (mustRoleEnum == null) {
return joinPoint.proceed();
}
// 必须有该权限才通过
UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUser_role());
if (userRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
// 如果被封号,直接拒绝
if (UserRoleEnum.BAN.equals(userRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
// 必须有管理员权限
if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {
// 用户没有管理员权限,拒绝
if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
// 通过权限校验,放行
return joinPoint.proceed();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 请求响应日志 AOP
**/
@Aspect
@Component
@Slf4j
public class LogInterceptor {

/**
* 执行拦截
*/
@Around("execution(* fun.wgfun.Omni.controller.*.*(..))")
public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
// 计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 获取请求路径
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
// 生成请求唯一 id
String requestId = UUID.randomUUID().toString();
String url = httpServletRequest.getRequestURI();
// 获取请求参数
Object[] args = point.getArgs();
String reqParam = "[" + StringUtils.join(args, ", ") + "]";
// 输出请求日志
log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
httpServletRequest.getRemoteHost(), reqParam);
// 执行原方法
Object result = point.proceed();
// 输出响应日志
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
return result;
}
}

通用类 common

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 通用返回类
* @param <T>
*/
@Data
public class BaseResponse<T> implements Serializable {
private int code;
private T data;
private String message;

public BaseResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}

public BaseResponse(int code, T data) {
this(code, data, "");
}

public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 删除请求
*/
@Data
public class DeleteRequest implements Serializable {

/**
* id
*/
private Long id;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 自定义错误码
*/
@Getter
public enum ErrorCode {
SUCCESS(0, "ok"),
PARAMS_ERROR(40000, "请求参数错误"),
NOT_LOGIN_ERROR(40100, "未登录"),
NO_AUTH_ERROR(40101, "无权限"),
NOT_FOUND_ERROR(40400, "请求数据不存在"),
FORBIDDEN_ERROR(40300, "禁止访问"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");

/**
* 状态码
*/
private final int code;

/**
* 信息
*/
private final String message;

ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 分页请求
*/
@Data
public class PageRequest {

/**
* 当前页号
*/
private int current = 1;

/**
* 页面大小
*/
private int pageSize = 10;

/**
* 排序字段
*/
private String sortField;

/**
* 排序顺序(默认升序)
*/
private String sortOrder = CommonConstant.SORT_ORDER_ASC;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 返回工具类
*/
public class ResultUtils {

/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}

/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}

/**
* 失败
*
* @param code
* @param message
* @return
*/
public static BaseResponse error(int code, String message) {
return new BaseResponse(code, null, message);
}

/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String message) {
return new BaseResponse(errorCode.getCode(), null, message);
}
}

配置类 config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 全局跨域配置
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
// 覆盖所有请求
registry.addMapping("/**")
// 允许发送 Cookie
.allowCredentials(true)
// 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 腾讯云对象存储客户端
*/
@Configuration //配置文件
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {
/**
* accessKey
*/
private String accessKey;
/**
* secretKey
*/
private String secretKey;
/**
* 区域
*/
private String region;
/**
* 桶名
*/
private String bucket;

@Bean
public COSClient cosClient() {
// 初始化用户身份信息(secretId, secretKey)
COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey);
// 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
ClientConfig clientConfig = new ClientConfig(new Region(region));
// 生成cos客户端
return new COSClient(cred, clientConfig);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Spring MVC JSON 配置:解决 Long 类型精度丢失问题
*/
@Configuration
public class JsonConfig {

/**
* 自定义 ObjectMapper(纯 Jackson 原生 API,无依赖问题)
* - 核心:Long/long 序列化为字符串,避免前端 JS 精度丢失
* - 兼容:自动处理 LocalDateTime 等时间类型
*/
@Bean
public ObjectMapper objectMapper() {
// 1. 创建自定义模块,注册 Long 序列化器(核心逻辑)
SimpleModule longToStringModule = new SimpleModule();
longToStringModule.addSerializer(Long.class, ToStringSerializer.instance); // 包装类 Long
longToStringModule.addSerializer(Long.TYPE, ToStringSerializer.instance); // 基本类型 long

// 2. 用 Jackson 原生 Builder 构建 ObjectMapper(Spring 7.0 推荐)
ObjectMapper objectMapper = com.fasterxml.jackson.databind.json.JsonMapper.builder()
// 注册时间模块(处理 LocalDateTime/LocalDate,可选但推荐)
.addModule(new JavaTimeModule())
// 注册自定义 Long 序列化模块(核心)
.addModule(longToStringModule)
// 可选:关闭时间戳序列化(让时间类型返回字符串,如 "2025-12-06 10:00:00")
.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();

return objectMapper;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* MyBatis Plus 配置
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 拦截器配置
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 微信开放平台配置
*/
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "wx.open")
@Data
public class WxOpenConfig {
private String appId;
private String appSecret;
private WxMpService wxMpService;

/**
* 单例模式(不用 @Bean 是为了防止和公众号的 service 冲突)
*/
public WxMpService getWxMpService() {
if (wxMpService != null) {
return wxMpService;
}
synchronized (this) {
if (wxMpService != null) {
return wxMpService;
}
WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
config.setAppId(appId);
config.setSecret(appSecret);
WxMpService service = new WxMpServiceImpl();
service.setWxMpConfigStorage(config);
wxMpService = service;
return wxMpService;
}
}
}

常量类 constant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 通用常量
*/
public interface CommonConstant {
/**
* 升序
*/
String SORT_ORDER_ASC = "ascend";

/**
* 降序
*/
String SORT_ORDER_DESC = "descend";
}

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 文件常量
*/
public interface FileConstant {

/**
* COS 访问地址
* todo 需替换配置
*/
String COS_HOST = "https://wgfun-1322866712.cos.ap-guangzhou.myqcloud.com";
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 用户常量
*/
public interface UserConstant {

/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "user_login";

// region 权限

/**
* 默认角色
*/
String DEFAULT_ROLE = "user";

/**
* 管理员角色
*/
String ADMIN_ROLE = "admin";

/**
* 被封号
*/
String BAN_ROLE = "ban";

// endregion
}

接口类 controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 文件接口
*/
@RestController
@RequestMapping("/file")
@Slf4j
public class FileController {

@Resource
private UserService userService;

@Resource
private CosManager cosManager;

/**
* 文件上传
*
* @param multipartFile
* @param uploadFileRequest
* @param request
* @return
*/
@PostMapping("/upload")
public BaseResponse<String> uploadFile(@RequestPart("file") MultipartFile multipartFile,
UploadFileRequest uploadFileRequest, HttpServletRequest request) {
String biz = uploadFileRequest.getBiz();
FileUploadBizEnum fileUploadBizEnum = FileUploadBizEnum.getEnumByValue(biz);
if (fileUploadBizEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
validFile(multipartFile, fileUploadBizEnum);
User loginUser = userService.getLoginUser(request);
// 文件目录:根据业务、用户来划分
String uuid = RandomStringUtils.randomAlphanumeric(8);
String filename = uuid + "-" + multipartFile.getOriginalFilename();
String filepath = String.format("/%s/%s/%s", fileUploadBizEnum.getValue(), loginUser.getId(), filename);
File file = null;
try {
// 上传文件
file = File.createTempFile(filepath, null);
multipartFile.transferTo(file);
cosManager.putObject(filepath, file);
// 返回可访问地址
return ResultUtils.success(FileConstant.COS_HOST + filepath);
} catch (Exception e) {
log.error("file upload error, filepath = " + filepath, e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
if (file != null) {
// 删除临时文件
boolean delete = file.delete();
if (!delete) {
log.error("file delete error, filepath = {}", filepath);
}
}
}
}

/**
* 校验文件
*
* @param multipartFile
* @param fileUploadBizEnum 业务类型
*/
private void validFile(MultipartFile multipartFile, FileUploadBizEnum fileUploadBizEnum) {
// 文件大小
long fileSize = multipartFile.getSize();
// 文件后缀
String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());
final long ONE_M = 1024 * 1024L;
if (FileUploadBizEnum.USER_AVATAR.equals(fileUploadBizEnum)) {
if (fileSize > ONE_M) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小不能超过 1M");
}
if (!Arrays.asList("jpeg", "jpg", "svg", "png", "webp").contains(fileSuffix)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件类型错误");
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/**
* 帖子接口
*/
@RestController
@RequestMapping("/post")
@Slf4j
public class PostController {

@Resource
private PostService postService;

@Resource
private UserService userService;

// region 增删改查

/**
* 创建
*
* @param postAddRequest
* @param request
* @return
*/
@PostMapping("/add")
public BaseResponse<Long> addPost(@RequestBody PostAddRequest postAddRequest, HttpServletRequest request) {
if (postAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Post post = new Post();
BeanUtils.copyProperties(postAddRequest, post);
List<String> tags = postAddRequest.getTags();
if (tags != null) {
post.setTags(JSONUtil.toJsonStr(tags));
}
postService.validPost(post, true);
User loginUser = userService.getLoginUser(request);
post.setUser_id(loginUser.getId());
post.setFavour_num(0);
post.setThumb_num(0);
boolean result = postService.save(post);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
long newPostId = post.getId();
return ResultUtils.success(newPostId);
}

/**
* 删除
*
* @param deleteRequest
* @param request
* @return
*/
@PostMapping("/delete")
public BaseResponse<Boolean> deletePost(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getLoginUser(request);
long id = deleteRequest.getId();
// 判断是否存在
Post oldPost = postService.getById(id);
ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);
// 仅本人或管理员可删除
if (!oldPost.getUser_id().equals(user.getId()) && !userService.isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
boolean b = postService.removeById(id);
return ResultUtils.success(b);
}

/**
* 更新(仅管理员)
*
* @param postUpdateRequest
* @return
*/
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updatePost(@RequestBody PostUpdateRequest postUpdateRequest) {
if (postUpdateRequest == null || postUpdateRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Post post = new Post();
BeanUtils.copyProperties(postUpdateRequest, post);
List<String> tags = postUpdateRequest.getTags();
if (tags != null) {
post.setTags(JSONUtil.toJsonStr(tags));
}
// 参数校验
postService.validPost(post, false);
long id = postUpdateRequest.getId();
// 判断是否存在
Post oldPost = postService.getById(id);
ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);
boolean result = postService.updateById(post);
return ResultUtils.success(result);
}

/**
* 根据 id 获取
*
* @param id
* @return
*/
@GetMapping("/get/vo")
public BaseResponse<PostVO> getPostVOById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Post post = postService.getById(id);
if (post == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
return ResultUtils.success(postService.getPostVO(post, request));
}

/**
* 分页获取列表(仅管理员)
*
* @param postQueryRequest
* @return
*/
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<Post>> listPostByPage(@RequestBody PostQueryRequest postQueryRequest) {
long current = postQueryRequest.getCurrent();
long size = postQueryRequest.getPageSize();
Page<Post> postPage = postService.page(new Page<>(current, size),
postService.getQueryWrapper(postQueryRequest));
return ResultUtils.success(postPage);
}

/**
* 分页获取列表(封装类)
*
* @param postQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page/vo")
public BaseResponse<Page<PostVO>> listPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,
HttpServletRequest request) {
long current = postQueryRequest.getCurrent();
long size = postQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Post> postPage = postService.page(new Page<>(current, size),
postService.getQueryWrapper(postQueryRequest));
return ResultUtils.success(postService.getPostVOPage(postPage, request));
}

/**
* 分页获取当前用户创建的资源列表
*
* @param postQueryRequest
* @param request
* @return
*/
@PostMapping("/my/list/page/vo")
public BaseResponse<Page<PostVO>> listMyPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,
HttpServletRequest request) {
if (postQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
postQueryRequest.setUserId(loginUser.getId());
long current = postQueryRequest.getCurrent();
long size = postQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Post> postPage = postService.page(new Page<>(current, size),
postService.getQueryWrapper(postQueryRequest));
return ResultUtils.success(postService.getPostVOPage(postPage, request));
}

// endregion

/**
* 分页搜索(从 ES 查询,封装类)
*
* @param postQueryRequest
* @param request
* @return
*/
@PostMapping("/search/page/vo")
public BaseResponse<Page<PostVO>> searchPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,
HttpServletRequest request) {
long size = postQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Post> postPage = postService.searchFromEs(postQueryRequest);
return ResultUtils.success(postService.getPostVOPage(postPage, request));
}

/**
* 编辑(用户)
*
* @param postEditRequest
* @param request
* @return
*/
@PostMapping("/edit")
public BaseResponse<Boolean> editPost(@RequestBody PostEditRequest postEditRequest, HttpServletRequest request) {
if (postEditRequest == null || postEditRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Post post = new Post();
BeanUtils.copyProperties(postEditRequest, post);
List<String> tags = postEditRequest.getTags();
if (tags != null) {
post.setTags(JSONUtil.toJsonStr(tags));
}
// 参数校验
postService.validPost(post, false);
User loginUser = userService.getLoginUser(request);
long id = postEditRequest.getId();
// 判断是否存在
Post oldPost = postService.getById(id);
ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);
// 仅本人或管理员可编辑
if (!oldPost.getUser_id().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
boolean result = postService.updateById(post);
return ResultUtils.success(result);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* 帖子收藏接口
*/
@RestController
@RequestMapping("/post_favour")
@Slf4j
public class PostFavourController {

@Resource
private PostFavourService postFavourService;

@Resource
private PostService postService;

@Resource
private UserService userService;

/**
* 收藏 / 取消收藏
*
* @param postFavourAddRequest
* @param request
* @return resultNum 收藏变化数
*/
@PostMapping("/")
public BaseResponse<Integer> doPostFavour(@RequestBody PostFavourAddRequest postFavourAddRequest,
HttpServletRequest request) {
if (postFavourAddRequest == null || postFavourAddRequest.getPostId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 登录才能操作
final User loginUser = userService.getLoginUser(request);
long postId = postFavourAddRequest.getPostId();
int result = postFavourService.doPostFavour(postId, loginUser);
return ResultUtils.success(result);
}

/**
* 获取我收藏的帖子列表
*
* @param postQueryRequest
* @param request
*/
@PostMapping("/my/list/page")
public BaseResponse<Page<PostVO>> listMyFavourPostByPage(@RequestBody PostQueryRequest postQueryRequest,
HttpServletRequest request) {
if (postQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
long current = postQueryRequest.getCurrent();
long size = postQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Post> postPage = postFavourService.listFavourPostByPage(new Page<>(current, size),
postService.getQueryWrapper(postQueryRequest), loginUser.getId());
return ResultUtils.success(postService.getPostVOPage(postPage, request));
}

/**
* 获取用户收藏的帖子列表
*
* @param postFavourQueryRequest
* @param request
*/
@PostMapping("/list/page")
public BaseResponse<Page<PostVO>> listFavourPostByPage(@RequestBody PostFavourQueryRequest postFavourQueryRequest,
HttpServletRequest request) {
if (postFavourQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long current = postFavourQueryRequest.getCurrent();
long size = postFavourQueryRequest.getPageSize();
Long userId = postFavourQueryRequest.getUserId();
// 限制爬虫
ThrowUtils.throwIf(size > 20 || userId == null, ErrorCode.PARAMS_ERROR);
Page<Post> postPage = postFavourService.listFavourPostByPage(new Page<>(current, size),
postService.getQueryWrapper(postFavourQueryRequest.getPostQueryRequest()), userId);
return ResultUtils.success(postService.getPostVOPage(postPage, request));
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 帖子点赞接口
*/
@RestController
@RequestMapping("/post_thumb")
@Slf4j
public class PostThumbController {

@Resource
private PostThumbService postThumbService;

@Resource
private UserService userService;

/**
* 点赞 / 取消点赞
*
* @param postThumbAddRequest
* @param request
* @return resultNum 本次点赞变化数
*/
@PostMapping("/")
public BaseResponse<Integer> doThumb(@RequestBody PostThumbAddRequest postThumbAddRequest,
HttpServletRequest request) {
if (postThumbAddRequest == null || postThumbAddRequest.getPostId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 登录才能点赞
final User loginUser = userService.getLoginUser(request);
long postId = postThumbAddRequest.getPostId();
int result = postThumbService.doPostThumb(postId, loginUser);
return ResultUtils.success(result);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/**
* 用户接口
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

@Resource
private UserService userService;

@Resource
private WxOpenConfig wxOpenConfig;

// region 登录相关

/**
* 用户注册
*
* @param userRegisterRequest
* @return
*/
@PostMapping("/register")
public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return null;
}
long result = userService.userRegister(userAccount, userPassword, checkPassword);
return ResultUtils.success(result);
}

/**
* 用户登录
*
* @param userLoginRequest
* @param request
* @return
*/
@PostMapping("/login")
public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);
return ResultUtils.success(loginUserVO);
}

/**
* 用户登录(微信开放平台)
*/
@GetMapping("/login/wx_open")
public BaseResponse<LoginUserVO> userLoginByWxOpen(HttpServletRequest request, HttpServletResponse response,
@RequestParam("code") String code) {
WxOAuth2AccessToken accessToken;
try {
WxMpService wxService = wxOpenConfig.getWxMpService();
accessToken = wxService.getOAuth2Service().getAccessToken(code);
WxOAuth2UserInfo userInfo = wxService.getOAuth2Service().getUserInfo(accessToken, code);
String unionId = userInfo.getUnionId();
String mpOpenId = userInfo.getOpenid();
if (StringUtils.isAnyBlank(unionId, mpOpenId)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
}
return ResultUtils.success(userService.userLoginByMpOpen(userInfo, request));
} catch (Exception e) {
log.error("userLoginByWxOpen error", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
}
}

/**
* 用户注销
*
* @param request
* @return
*/
@PostMapping("/logout")
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean result = userService.userLogout(request);
return ResultUtils.success(result);
}

/**
* 获取当前登录用户
*
* @param request
* @return
*/
@GetMapping("/get/login")
public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {
User user = userService.getLoginUser(request);
return ResultUtils.success(userService.getLoginUserVO(user));
}

// endregion

// region 增删改查

/**
* 创建用户
*
* @param userAddRequest
* @param request
* @return
*/
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) {
if (userAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = new User();
BeanUtils.copyProperties(userAddRequest, user);
// 默认密码 12345678
String defaultPassword = "12345678";
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + defaultPassword).getBytes());
user.setUser_password(encryptPassword);
boolean result = userService.save(user);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
return ResultUtils.success(user.getId());
}

/**
* 删除用户
*
* @param deleteRequest
* @param request
* @return
*/
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean b = userService.removeById(deleteRequest.getId());
return ResultUtils.success(b);
}

/**
* 更新用户
*
* @param userUpdateRequest
* @param request
* @return
*/
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest,
HttpServletRequest request) {
if (userUpdateRequest == null || userUpdateRequest.getId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = new User();
BeanUtils.copyProperties(userUpdateRequest, user);
boolean result = userService.updateById(user);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
return ResultUtils.success(true);
}

/**
* 根据 id 获取用户(仅管理员)
*
* @param id
* @param request
* @return
*/
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<User> getUserById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getById(id);
ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);
return ResultUtils.success(user);
}

/**
* 根据 id 获取包装类
*
* @param id
* @param request
* @return
*/
@GetMapping("/get/vo")
public BaseResponse<UserVO> getUserVOById(long id, HttpServletRequest request) {
BaseResponse<User> response = getUserById(id, request);
User user = response.getData();
return ResultUtils.success(userService.getUserVO(user));
}

/**
* 分页获取用户列表(仅管理员)
*
* @param userQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<User>> listUserByPage(@RequestBody UserQueryRequest userQueryRequest,
HttpServletRequest request) {
long current = userQueryRequest.getCurrent();
long size = userQueryRequest.getPageSize();
Page<User> userPage = userService.page(new Page<>(current, size),
userService.getQueryWrapper(userQueryRequest));
return ResultUtils.success(userPage);
}

/**
* 分页获取用户封装列表
*
* @param userQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page/vo")
public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest,
HttpServletRequest request) {
if (userQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long current = userQueryRequest.getCurrent();
long size = userQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<User> userPage = userService.page(new Page<>(current, size),
userService.getQueryWrapper(userQueryRequest));
Page<UserVO> userVOPage = new Page<>(current, size, userPage.getTotal());
List<UserVO> userVO = userService.getUserVO(userPage.getRecords());
userVOPage.setRecords(userVO);
return ResultUtils.success(userVOPage);
}

// endregion

/**
* 更新个人信息
*
* @param userUpdateMyRequest
* @param request
* @return
*/
@PostMapping("/update/my")
public BaseResponse<Boolean> updateMyUser(@RequestBody UserUpdateMyRequest userUpdateMyRequest,
HttpServletRequest request) {
if (userUpdateMyRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
User user = new User();
BeanUtils.copyProperties(userUpdateMyRequest, user);
user.setId(loginUser.getId());
boolean result = userService.updateById(user);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
return ResultUtils.success(true);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
* 微信公众号相关接口
**/
@RestController
@RequestMapping("/")
@Slf4j
public class WxMpController {

@Resource
private WxMpService wxMpService;

@Resource
private WxMpMessageRouter router;

@PostMapping("/")
public void receiveMessage(HttpServletRequest request, HttpServletResponse response)
throws IOException {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
// 校验消息签名,判断是否为公众平台发的消息
String signature = request.getParameter("signature");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
response.getWriter().println("非法请求");
}
// 加密类型
String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ? "raw"
: request.getParameter("encrypt_type");
// 明文消息
if ("raw".equals(encryptType)) {
return;
}
// aes 加密消息
if ("aes".equals(encryptType)) {
// 解密消息
String msgSignature = request.getParameter("msg_signature");
WxMpXmlMessage inMessage = WxMpXmlMessage
.fromEncryptedXml(request.getInputStream(), wxMpService.getWxMpConfigStorage(), timestamp,
nonce,
msgSignature);
log.info("message content = {}", inMessage.getContent());
// 路由消息并处理
WxMpXmlOutMessage outMessage = router.route(inMessage);
if (outMessage == null) {
response.getWriter().write("");
} else {
response.getWriter().write(outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage()));
}
return;
}
response.getWriter().println("不可识别的加密类型");
}

@GetMapping("/")
public String check(String timestamp, String nonce, String signature, String echostr) {
log.info("check");
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
return echostr;
} else {
return "";
}
}

/**
* 设置公众号菜单
*
* @return
* @throws WxErrorException
*/
@GetMapping("/setMenu")
public String setMenu() throws WxErrorException {
log.info("setMenu");
WxMenu wxMenu = new WxMenu();
// 菜单一
WxMenuButton wxMenuButton1 = new WxMenuButton();
wxMenuButton1.setType(MenuButtonType.VIEW);
wxMenuButton1.setName("主菜单一");
// 子菜单
WxMenuButton wxMenuButton1SubButton1 = new WxMenuButton();
wxMenuButton1SubButton1.setType(MenuButtonType.VIEW);
wxMenuButton1SubButton1.setName("跳转页面");
wxMenuButton1SubButton1.setUrl(
"https://yupi.icu");
wxMenuButton1.setSubButtons(Collections.singletonList(wxMenuButton1SubButton1));

// 菜单二
WxMenuButton wxMenuButton2 = new WxMenuButton();
wxMenuButton2.setType(MenuButtonType.CLICK);
wxMenuButton2.setName("点击事件");
wxMenuButton2.setKey(WxMpConstant.CLICK_MENU_KEY);

// 菜单三
WxMenuButton wxMenuButton3 = new WxMenuButton();
wxMenuButton3.setType(MenuButtonType.VIEW);
wxMenuButton3.setName("主菜单三");
WxMenuButton wxMenuButton3SubButton1 = new WxMenuButton();
wxMenuButton3SubButton1.setType(MenuButtonType.VIEW);
wxMenuButton3SubButton1.setName("编程学习");
wxMenuButton3SubButton1.setUrl("https://yupi.icu");
wxMenuButton3.setSubButtons(Collections.singletonList(wxMenuButton3SubButton1));

// 设置主菜单
wxMenu.setButtons(Arrays.asList(wxMenuButton1, wxMenuButton2, wxMenuButton3));
wxMpService.getMenuService().menuCreate(wxMenu);
return "ok";
}
}

esdao

1
2
3
4
5
6
7
/**
* 帖子 ES 操作

*/
public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> {
List<PostEsDTO> findByUserId(Long userId);
}

异常处理类 exception

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 自定义异常类
*/
@Getter
public class BusinessException extends RuntimeException {

/**
* 错误码
*/
private final int code;

public BusinessException(int code, String message) {
super(message);
this.code = code;
}

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}

public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("BusinessException", e);
return ResultUtils.error(e.getCode(), e.getMessage());
}

@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
log.error("RuntimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 抛异常工具类
*/
public class ThrowUtils {

/**
* 条件成立则抛异常
*
* @param condition
* @param runtimeException
*/
public static void throwIf(boolean condition, RuntimeException runtimeException) {
if (condition) {
throw runtimeException;
}
}

/**
* 条件成立则抛异常
*
* @param condition
* @param errorCode
*/
public static void throwIf(boolean condition, ErrorCode errorCode) {
throwIf(condition, new BusinessException(errorCode));
}

/**
* 条件成立则抛异常
*
* @param condition
* @param errorCode
* @param message
*/
public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
throwIf(condition, new BusinessException(errorCode, message));
}
}

代码生成模板 generate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* 代码生成器
*/
public class CodeGenerator {

/**
* 用法:修改生成参数和生成路径,注释掉不需要的生成逻辑,然后运行即可
*
* @param args
* @throws TemplateException
* @throws IOException
*/
public static void main(String[] args) throws TemplateException, IOException {
// 指定生成参数
String packageName = "com.yupi.springbootinit";
String dataName = "用户评论";
String dataKey = "UserComment";
String upperDataKey = "UserComment";

// 封装生成参数
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("packageName", packageName);
dataModel.put("dataName", dataName);
dataModel.put("dataKey", dataKey);
dataModel.put("upperDataKey", upperDataKey);

// 生成路径默认值
String projectPath = System.getProperty("user.dir");
// 参考路径,可以自己调整下面的 outputPath
String inputPath = projectPath + File.separator + "src/main/resources/templates/模板名称.java.ftl";
String outputPath = String.format("%s/generator/包名/%s类后缀.java", projectPath, upperDataKey);

// 1、生成 Controller
// 指定生成路径
inputPath = projectPath + File.separator + "src/main/resources/templates/TemplateController.java.ftl";
outputPath = String.format("%s/generator/controller/%sController.java", projectPath, upperDataKey);
// 生成
doGenerate(inputPath, outputPath, dataModel);
System.out.println("生成 Controller 成功,文件路径:" + outputPath);

// 2、生成 Service 接口和实现类
// 生成 Service 接口
inputPath = projectPath + File.separator + "src/main/resources/templates/TemplateService.java.ftl";
outputPath = String.format("%s/generator/service/%sService.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
System.out.println("生成 Service 接口成功,文件路径:" + outputPath);
// 生成 Service 实现类
inputPath = projectPath + File.separator + "src/main/resources/templates/TemplateServiceImpl.java.ftl";
outputPath = String.format("%s/generator/service/impl/%sServiceImpl.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
System.out.println("生成 Service 实现类成功,文件路径:" + outputPath);

// 3、生成数据模型封装类(包括 DTO 和 VO)
// 生成 DTO
inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateAddRequest.java.ftl";
outputPath = String.format("%s/generator/model/dto/%sAddRequest.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateQueryRequest.java.ftl";
outputPath = String.format("%s/generator/model/dto/%sQueryRequest.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateEditRequest.java.ftl";
outputPath = String.format("%s/generator/model/dto/%sEditRequest.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateUpdateRequest.java.ftl";
outputPath = String.format("%s/generator/model/dto/%sUpdateRequest.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
System.out.println("生成 DTO 成功,文件路径:" + outputPath);
// 生成 VO
inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateVO.java.ftl";
outputPath = String.format("%s/generator/model/vo/%sVO.java", projectPath, upperDataKey);
doGenerate(inputPath, outputPath, dataModel);
System.out.println("生成 VO 成功,文件路径:" + outputPath);
}

/**
* 生成文件
*
* @param inputPath 模板文件输入路径
* @param outputPath 输出路径
* @param model 数据模型
* @throws IOException
* @throws TemplateException
*/
public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException {
// new 出 Configuration 对象,参数为 FreeMarker 版本号
Configuration configuration = new Configuration(Configuration.VERSION_2_3_31);

// 指定模板文件所在的路径
File templateDir = new File(inputPath).getParentFile();
configuration.setDirectoryForTemplateLoading(templateDir);

// 设置模板文件使用的字符集
configuration.setDefaultEncoding("utf-8");

// 创建模板对象,加载指定模板
String templateName = new File(inputPath).getName();
Template template = configuration.getTemplate(templateName);

// 文件不存在则创建文件和父目录
if (!FileUtil.exist(outputPath)) {
FileUtil.touch(outputPath);
}

// 生成
Writer out = new FileWriter(outputPath);
template.process(model, out);

// 生成文件后别忘了关闭哦
out.close();
}
}

定时任务 job

cycle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 增量同步帖子到 es
*/
// todo 取消注释开启任务
@Component
@Slf4j
public class IncSyncPostToEs {

@Resource
private PostMapper postMapper;

@Resource
private PostEsDao postEsDao;

/**
* 每分钟执行一次
*/
@Scheduled(fixedRate = 60 * 1000)
public void run() {
// 查询近 5 分钟内的数据
Date fiveMinutesAgoDate = new Date(new Date().getTime() - 5 * 60 * 1000L);
List<Post> postList = postMapper.listPostWithDelete(fiveMinutesAgoDate);
if (CollUtil.isEmpty(postList)) {
log.info("no inc post");
return;
}
List<PostEsDTO> postEsDTOList = postList.stream()
.map(PostEsDTO::objToDto)
.collect(Collectors.toList());
final int pageSize = 500;
int total = postEsDTOList.size();
log.info("IncSyncPostToEs start, total {}", total);
for (int i = 0; i < total; i += pageSize) {
int end = Math.min(i + pageSize, total);
log.info("sync from {} to {}", i, end);
postEsDao.saveAll(postEsDTOList.subList(i, end));
}
log.info("IncSyncPostToEs end, total {}", total);
}
}

once

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 全量同步帖子到 es
*/
// todo 取消注释开启任务
@Component
@Slf4j
public class FullSyncPostToEs implements CommandLineRunner {

@Resource
private PostService postService;

@Resource
private PostEsDao postEsDao;

@Override
public void run(String... args) {
List<Post> postList = postService.list();
if (CollUtil.isEmpty(postList)) {
return;
}
List<PostEsDTO> postEsDTOList = postList.stream().map(PostEsDTO::objToDto).collect(Collectors.toList());
final int pageSize = 500;
int total = postEsDTOList.size();
log.info("FullSyncPostToEs start, total {}", total);
for (int i = 0; i < total; i += pageSize) {
int end = Math.min(i + pageSize, total);
log.info("sync from {} to {}", i, end);
postEsDao.saveAll(postEsDTOList.subList(i, end));
}
log.info("FullSyncPostToEs end, total {}", total);
}
}

manger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Cos 对象存储操作
*/
@Component
public class CosManager {
@Resource
private CosClientConfig cosClientConfig;
@Resource
private COSClient cosClient;

/**
* 上传对象
*
* @param key 唯一键
* @param localFilePath 本地文件路径
* @return
*/
public PutObjectResult putObject(String key, String localFilePath) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
new File(localFilePath));
return cosClient.putObject(putObjectRequest);
}

/**
* 上传对象
*
* @param key 唯一键
* @param file 文件
* @return
*/
public PutObjectResult putObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
return cosClient.putObject(putObjectRequest);
}
}

mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 帖子收藏数据库操作
*/
public interface PostFavourMapper extends BaseMapper<PostFavour> {

/**
* 分页查询收藏帖子列表
*
* @param page
* @param queryWrapper
* @param favourUserId
* @return
*/
Page<Post> listFavourPostByPage(IPage<Post> page, @Param(Constants.WRAPPER) Wrapper<Post> queryWrapper,
long favourUserId);

}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 帖子数据库操作
*/
public interface PostMapper extends BaseMapper<Post> {

/**
* 查询帖子列表(包括已被删除的数据)
*/
List<Post> listPostWithDelete(Date minUpdateTime);

}

1
2
3
4
5
6
/**
* 帖子点赞数据库操作
*/
public interface PostThumbMapper extends BaseMapper<PostThumb> {

}
1
2
3
4
5
6
7
/**
* 用户数据库操作
*/
public interface UserMapper extends BaseMapper<User> {

}

model

dto

file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 文件上传请求
*/
@Data
public class UploadFileRequest implements Serializable {

/**
* 业务
*/
private String biz;

@Serial
private static final long serialVersionUID = 1L;
}

post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 创建请求
*/
@Data
public class PostAddRequest implements Serializable {

/**
* 标题
*/
private String title;

/**
* 内容
*/
private String content;

/**
* 标签列表
*/
private List<String> tags;

@Serial
private static final long serialVersionUID = 1L;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 编辑请求
*/
@Data
public class PostEditRequest implements Serializable {

/**
* id
*/
private Long id;

/**
* 标题
*/
private String title;

/**
* 内容
*/
private String content;

/**
* 标签列表
*/
private List<String> tags;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* 帖子 ES 数据传输对象(适配 Elasticsearch Java API Client)
* 注意:不再使用 Spring Data Elasticsearch 注解
*/
@Data
public class PostEsDTO implements Serializable {

@Serial
private static final long serialVersionUID = 1L;

/**
* id
*/
@JsonProperty("id")
private Long id;

/**
* 标题
*/
@JsonProperty("title")
private String title;

/**
* 内容
*/
@JsonProperty("content")
private String content;

/**
* 标签列表
*/
@JsonProperty("tags")
private List<String> tags;

/**
* 点赞数
*/
@JsonProperty("thumbNum")
private Integer thumbNum;

/**
* 收藏数
*/
@JsonProperty("favourNum")
private Integer favourNum;

/**
* 创建用户 id
*/
@JsonProperty("userId")
private Long userId;

/**
* 创建时间(ES 中为 date 类型,格式 yyyy-MM-dd'T'HH:mm:ss.SSS'Z')
*/
@JsonProperty("createTime")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date createTime;

/**
* 更新时间
*/
@JsonProperty("updateTime")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date updateTime;

/**
* 是否删除(0-未删除,1-已删除)
*/
@JsonProperty("isDelete")
private Integer isDelete;

/**
* 对象转包装类
*/
public static PostEsDTO objToDto(Post post) {
if (post == null) {
return null;
}
PostEsDTO postEsDTO = new PostEsDTO();
BeanUtils.copyProperties(post, postEsDTO);
String tagsStr = post.getTags();
if (StringUtils.isNotBlank(tagsStr)) {
postEsDTO.setTags(JSONUtil.toList(tagsStr, String.class));
}
return postEsDTO;
}

/**
* 包装类转对象
*/
public static Post dtoToObj(PostEsDTO postEsDTO) {
if (postEsDTO == null) {
return null;
}
Post post = new Post();
BeanUtils.copyProperties(postEsDTO, post);
List<String> tagList = postEsDTO.getTags();
if (CollUtil.isNotEmpty(tagList)) {
post.setTags(JSONUtil.toJsonStr(tagList));
}
return post;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 查询请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class PostQueryRequest extends PageRequest implements Serializable {

/**
* id
*/
private Long id;

/**
* id
*/
private Long notId;

/**
* 搜索词
*/
private String searchText;

/**
* 标题
*/
private String title;

/**
* 内容
*/
private String content;

/**
* 标签列表
*/
private List<String> tags;

/**
* 至少有一个标签
*/
private List<String> orTags;

/**
* 创建用户 id
*/
private Long userId;

/**
* 收藏用户 id
*/
private Long favourUserId;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 更新请求
*/
@Data
public class PostUpdateRequest implements Serializable {

/**
* id
*/
private Long id;

/**
* 标题
*/
private String title;

/**
* 内容
*/
private String content;

/**
* 标签列表
*/
private List<String> tags;

@Serial
private static final long serialVersionUID = 1L;
}

postfavour

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 帖子收藏 / 取消收藏请求
*/
@Data
public class PostFavourAddRequest implements Serializable {

/**
* 帖子 id
*/
private Long postId;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 帖子收藏查询请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PostFavourQueryRequest extends PageRequest implements Serializable {

/**
* 帖子查询请求
*/
private PostQueryRequest postQueryRequest;

/**
* 用户 id
*/
private Long userId;

@Serial
private static final long serialVersionUID = 1L;
}

postthumb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 帖子点赞请求
*/
@Data
public class PostThumbAddRequest implements Serializable {

/**
* 帖子 id
*/
private Long postId;

@Serial
private static final long serialVersionUID = 1L;
}

user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 用户创建请求
*/
@Data
public class UserAddRequest implements Serializable {

/**
* 用户昵称
*/
private String userName;

/**
* 账号
*/
private String userAccount;

/**
* 用户头像
*/
private String userAvatar;

/**
* 用户角色: user, admin
*/
private String userRole;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
/**
* 用户登录请求
*/
@Data
public class UserLoginRequest implements Serializable {
@Serial
private static final long serialVersionUID = 3191241716373120793L;
private String userAccount;
private String userPassword;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 用户查询请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {
/**
* id
*/
private Long id;

/**
* 开放平台id
*/
private String unionId;

/**
* 公众号openId
*/
private String mpOpenId;

/**
* 用户昵称
*/
private String userName;

/**
* 简介
*/
private String userProfile;

/**
* 用户角色:user/admin/ban
*/
private String userRole;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 用户注册请求体
*/
@Data
public class UserRegisterRequest implements Serializable {
@Serial
private static final long serialVersionUID = 3191241716373120793L;
private String userAccount;
private String userPassword;
private String checkPassword;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 用户更新个人信息请求
*/
@Data
public class UserUpdateMyRequest implements Serializable {

/**
* 用户昵称
*/
private String userName;

/**
* 用户头像
*/
private String userAvatar;

/**
* 简介
*/
private String userProfile;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 用户更新请求
*/
@Data
public class UserUpdateRequest implements Serializable {
/**
* id
*/
private Long id;

/**
* 用户昵称
*/
private String userName;

/**
* 用户头像
*/
private String userAvatar;

/**
* 简介
*/
private String userProfile;

/**
* 用户角色:user/admin/ban
*/
private String userRole;

@Serial
private static final long serialVersionUID = 1L;
}

entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 帖子
*/
@TableName(value = "post")
@Data
public class Post implements Serializable {

/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;

/**
* 标题
*/
private String title;

/**
* 内容
*/
private String content;

/**
* 标签列表 json
*/
private String tags;

/**
* 点赞数
*/
private Integer thumb_num;

/**
* 收藏数
*/
private Integer favour_num;

/**
* 创建用户 id
*/
private Long user_id;

/**
* 创建时间
*/
private Date create_time;

/**
* 更新时间
*/
private Date update_time;

/**
* 是否删除
*/
@TableLogic
private Integer is_delete;

@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 帖子收藏
**/
@TableName(value = "post_favour")
@Data
public class PostFavour implements Serializable {

/**
* id
*/
@TableId(type = IdType.AUTO)
private Long id;

/**
* 帖子 id
*/
private Long post_id;

/**
* 创建用户 id
*/
private Long user_id;

/**
* 创建时间
*/
private Date create_time;

/**
* 更新时间
*/
private Date update_time;

@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 帖子点赞
*/
@TableName(value = "post_thumb")
@Data
public class PostThumb implements Serializable {

/**
* id
*/
@TableId(type = IdType.AUTO)
private Long id;

/**
* 帖子 id
*/
private Long post_id;

/**
* 创建用户 id
*/
private Long user_id;

/**
* 创建时间
*/
private Date create_time;

/**
* 更新时间
*/
private Date update_time;

@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 用户
*/
@TableName(value = "user")
@Data
public class User implements Serializable {

/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;

/**
* 用户账号
*/
private String user_account;

/**
* 用户密码
*/
private String user_password;

/**
* 开放平台id
*/
private String union_id;

/**
* 公众号openId
*/
private String mpOpen_id;

/**
* 用户昵称
*/
private String user_name;

/**
* 用户头像
*/
private String user_avatar;

/**
* 用户简介
*/
private String user_profile;

/**
* 用户角色:user/admin/ban
*/
private String user_role;

/**
* 创建时间
*/
private Date create_time;

/**
* 更新时间
*/
private Date update_time;

/**
* 是否删除
*/
@TableLogic
private Integer is_delete;

@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

enums

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 文件上传业务类型枚举
*/
public enum FileUploadBizEnum {

USER_AVATAR("用户头像", "user_avatar");

private final String text;

private final String value;

FileUploadBizEnum(String text, String value) {
this.text = text;
this.value = value;
}

/**
* 获取值列表
*
* @return 值列表
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}

/**
* 根据 value 获取枚举
*
* @param value
* @return 枚举
*/
public static FileUploadBizEnum getEnumByValue(String value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (FileUploadBizEnum anEnum : FileUploadBizEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}

public String getValue() {
return value;
}

public String getText() {
return text;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 用户角色枚举
*/
public enum UserRoleEnum {

USER("用户", "user"),
ADMIN("管理员", "admin"),
BAN("被封号", "ban");

private final String text;

private final String value;

UserRoleEnum(String text, String value) {
this.text = text;
this.value = value;
}

/**
* 获取值列表
*
* @return 值列表
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}

/**
* 根据 value 获取枚举
*
* @param value
* @return 枚举
*/
public static UserRoleEnum getEnumByValue(String value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (UserRoleEnum anEnum : UserRoleEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}

public String getValue() {
return value;
}

public String getText() {
return text;
}
}

vo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 已登录用户视图(脱敏)
**/
@Data
public class LoginUserVO implements Serializable {

/**
* 用户 id
*/
private Long id;

/**
* 用户昵称
*/
private String userName;

/**
* 用户头像
*/
private String userAvatar;

/**
* 用户简介
*/
private String userProfile;

/**
* 用户角色:user/admin/ban
*/
private String userRole;

/**
* 创建时间
*/
private Date createTime;

/**
* 更新时间
*/
private Date updateTime;

@Serial
private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 帖子视图
*/
@Data
public class PostVO implements Serializable {

/**
* id
*/
private Long id;

/**
* 标题
*/
private String title;

/**
* 内容
*/
private String content;

/**
* 点赞数
*/
private Integer thumbNum;

/**
* 收藏数
*/
private Integer favourNum;

/**
* 创建用户 id
*/
private Long userId;

/**
* 创建时间
*/
private Date createTime;

/**
* 更新时间
*/
private Date updateTime;

/**
* 标签列表
*/
private List<String> tagList;

/**
* 创建人信息
*/
private UserVO user;

/**
* 是否已点赞
*/
private Boolean hasThumb;

/**
* 是否已收藏
*/
private Boolean hasFavour;

/**
* 包装类转对象
*
* @param postVO
* @return Post
*/
public static Post voToObj(PostVO postVO) {
if (postVO == null) {
return null;
}
Post post = new Post();
BeanUtils.copyProperties(postVO, post);
List<String> tagList = postVO.getTagList();
post.setTags(JSONUtil.toJsonStr(tagList));
return post;
}

/**
* 对象转包装类
*
* @param post
* @return
*/
public static PostVO objToVo(Post post) {
if (post == null) {
return null;
}
PostVO postVO = new PostVO();
BeanUtils.copyProperties(post, postVO);
postVO.setTagList(JSONUtil.toList(post.getTags(), String.class));
return postVO;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 用户视图(脱敏)
*/
@Data
public class UserVO implements Serializable {

/**
* id
*/
private Long id;

/**
* 用户昵称
*/
private String userName;

/**
* 用户头像
*/
private String userAvatar;

/**
* 用户简介
*/
private String userProfile;

/**
* 用户角色:user/admin/ban
*/
private String userRole;

/**
* 创建时间
*/
private Date createTime;

@Serial
private static final long serialVersionUID = 1L;
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 帖子收藏服务
*/
public interface PostFavourService extends IService<PostFavour> {

/**
* 帖子收藏
*
* @param postId
* @param loginUser
* @return
*/
int doPostFavour(long postId, User loginUser);

/**
* 分页获取用户收藏的帖子列表
*
* @param page
* @param queryWrapper
* @param favourUserId
* @return
*/
Page<Post> listFavourPostByPage(IPage<Post> page, Wrapper<Post> queryWrapper, long favourUserId);

/**
* 帖子收藏(内部服务)
*
* @param userId
* @param postId
* @return
*/
int doPostFavourInner(long userId, long postId);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 帖子服务
*/
public interface PostService extends IService<Post> {

/**
* 校验
*
* @param post
* @param add
*/
void validPost(Post post, boolean add);

/**
* 获取查询条件
*
* @param postQueryRequest
* @return
*/
QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest);

/**
* 从 ES 查询
*
* @param postQueryRequest
* @return
*/
Page<Post> searchFromEs(PostQueryRequest postQueryRequest);

/**
* 获取帖子封装
*
* @param post
* @param request
* @return
*/
PostVO getPostVO(Post post, HttpServletRequest request);

/**
* 分页获取帖子封装
*
* @param postPage
* @param request
* @return
*/
Page<PostVO> getPostVOPage(Page<Post> postPage, HttpServletRequest request);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 帖子点赞服务
*/
public interface PostThumbService extends IService<PostThumb> {

/**
* 点赞
*
* @param postId
* @param loginUser
* @return
*/
int doPostThumb(long postId, User loginUser);

/**
* 帖子点赞(内部服务)
*
* @param userId
* @param postId
* @return
*/
int doPostThumbInner(long userId, long postId);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* 用户服务
*/
public interface UserService extends IService<User> {

/**
* 用户注册
*
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param checkPassword 校验密码
* @return 新用户 id
*/
long userRegister(String userAccount, String userPassword, String checkPassword);

/**
* 用户登录
*
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param request
* @return 脱敏后的用户信息
*/
LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request);

/**
* 用户登录(微信开放平台)
*
* @param wxOAuth2UserInfo 从微信获取的用户信息
* @param request
* @return 脱敏后的用户信息
*/
LoginUserVO userLoginByMpOpen(WxOAuth2UserInfo wxOAuth2UserInfo, HttpServletRequest request);

/**
* 获取当前登录用户
*
* @param request
* @return
*/
User getLoginUser(HttpServletRequest request);

/**
* 获取当前登录用户(允许未登录)
*
* @param request
* @return
*/
User getLoginUserPermitNull(HttpServletRequest request);

/**
* 是否为管理员
*
* @param request
* @return
*/
boolean isAdmin(HttpServletRequest request);

/**
* 是否为管理员
*
* @param user
* @return
*/
boolean isAdmin(User user);

/**
* 用户注销
*
* @param request
* @return
*/
boolean userLogout(HttpServletRequest request);

/**
* 获取脱敏的已登录用户信息
*
* @return
*/
LoginUserVO getLoginUserVO(User user);

/**
* 获取脱敏的用户信息
*
* @param user
* @return
*/
UserVO getUserVO(User user);

/**
* 获取脱敏的用户信息
*
* @param userList
* @return
*/
List<UserVO> getUserVO(List<User> userList);

/**
* 获取查询条件
*
* @param userQueryRequest
* @return
*/
QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest);

}

impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 帖子收藏服务实现
*/
@Service
public class PostFavourServiceImpl extends ServiceImpl<PostFavourMapper, PostFavour>
implements PostFavourService {

@Resource
private PostService postService;

/**
* 帖子收藏
*
* @param postId
* @param loginUser
* @return
*/
@Override
public int doPostFavour(long postId, User loginUser) {
// 判断是否存在
Post post = postService.getById(postId);
if (post == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 是否已帖子收藏
long userId = loginUser.getId();
// 每个用户串行帖子收藏
// 锁必须要包裹住事务方法
PostFavourService postFavourService = (PostFavourService) AopContext.currentProxy();
synchronized (String.valueOf(userId).intern()) {
return postFavourService.doPostFavourInner(userId, postId);
}
}

@Override
public Page<Post> listFavourPostByPage(IPage<Post> page, Wrapper<Post> queryWrapper, long favourUserId) {
if (favourUserId <= 0) {
return new Page<>();
}
return baseMapper.listFavourPostByPage(page, queryWrapper, favourUserId);
}

/**
* 封装了事务的方法
*
* @param userId
* @param postId
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int doPostFavourInner(long userId, long postId) {
PostFavour postFavour = new PostFavour();
postFavour.setUser_id(userId);
postFavour.setPost_id(postId);
QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>(postFavour);
PostFavour oldPostFavour = this.getOne(postFavourQueryWrapper);
boolean result;
// 已收藏
if (oldPostFavour != null) {
result = this.remove(postFavourQueryWrapper);
if (result) {
// 帖子收藏数 - 1
result = postService.update()
.eq("id", postId)
.gt("favourNum", 0)
.setSql("favourNum = favourNum - 1")
.update();
return result ? -1 : 0;
} else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
} else {
// 未帖子收藏
result = this.save(postFavour);
if (result) {
// 帖子收藏数 + 1
result = postService.update()
.eq("id", postId)
.setSql("favourNum = favourNum + 1")
.update();
return result ? 1 : 0;
} else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/**
* 帖子服务实现(适配 Elasticsearch Java API Client)
*/
@Service
@Slf4j
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {

@Resource
private UserService userService;

@Resource
private PostThumbMapper postThumbMapper;

@Resource
private PostFavourMapper postFavourMapper;

// 使用新版 Elasticsearch Java API Client
@Resource
private ElasticsearchClient elasticsearchClient;

@Override
public void validPost(Post post, boolean add) {
if (post == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String title = post.getTitle();
String content = post.getContent();
String tags = post.getTags();
if (add) {
ThrowUtils.throwIf(StringUtils.isAnyBlank(title, content, tags), ErrorCode.PARAMS_ERROR);
}
if (StringUtils.isNotBlank(title) && title.length() > 80) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "标题过长");
}
if (StringUtils.isNotBlank(content) && content.length() > 8192) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "内容过长");
}
}

@Override
public QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest) {
QueryWrapper<Post> queryWrapper = new QueryWrapper<>();
if (postQueryRequest == null) {
return queryWrapper;
}
String searchText = postQueryRequest.getSearchText();
String sortField = postQueryRequest.getSortField();
String sortOrder = postQueryRequest.getSortOrder();
Long id = postQueryRequest.getId();
String title = postQueryRequest.getTitle();
String content = postQueryRequest.getContent();
List<String> tagList = postQueryRequest.getTags();
Long userId = postQueryRequest.getUserId();
Long notId = postQueryRequest.getNotId();

if (StringUtils.isNotBlank(searchText)) {
queryWrapper.and(qw -> qw.like("title", searchText).or().like("content", searchText));
}
queryWrapper.like(StringUtils.isNotBlank(title), "title", title);
queryWrapper.like(StringUtils.isNotBlank(content), "content", content);
if (CollUtil.isNotEmpty(tagList)) {
for (String tag : tagList) {
queryWrapper.like("tags", "\"" + tag + "\"");
}
}
queryWrapper.ne(ObjectUtils.isNotEmpty(notId), "id", notId);
queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
sortField);
return queryWrapper;
}

@Override
public Page<Post> searchFromEs(PostQueryRequest postQueryRequest) {
Long id = postQueryRequest.getId();
Long notId = postQueryRequest.getNotId();
String searchText = postQueryRequest.getSearchText();
String title = postQueryRequest.getTitle();
String content = postQueryRequest.getContent();
List<String> tagList = postQueryRequest.getTags();
List<String> orTagList = postQueryRequest.getOrTags();
Long userId = postQueryRequest.getUserId();
long current = postQueryRequest.getCurrent() - 1;
long pageSize = postQueryRequest.getPageSize();
String sortField = postQueryRequest.getSortField();
String sortOrder = postQueryRequest.getSortOrder();

BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();

// isDelete = 0
boolQueryBuilder.filter(TermQuery.of(t -> t.field("isDelete").value(0))._toQuery());

if (id != null) {
boolQueryBuilder.filter(TermQuery.of(t -> t.field("id").value(id))._toQuery());
}
if (notId != null) {
boolQueryBuilder.mustNot(TermQuery.of(t -> t.field("id").value(notId))._toQuery());
}
if (userId != null) {
boolQueryBuilder.filter(TermQuery.of(t -> t.field("userId").value(userId))._toQuery());
}

// 必须包含所有标签(AND)
if (CollUtil.isNotEmpty(tagList)) {
for (String tag : tagList) {
boolQueryBuilder.filter(TermQuery.of(t -> t.field("tags").value(tag))._toQuery());
}
}

// 包含任一标签(OR)
if (CollUtil.isNotEmpty(orTagList)) {
BoolQuery.Builder orTagBuilder = new BoolQuery.Builder();
for (String tag : orTagList) {
orTagBuilder.should(TermQuery.of(t -> t.field("tags").value(tag))._toQuery());
}
orTagBuilder.minimumShouldMatch("1");
boolQueryBuilder.filter(orTagBuilder.build()._toQuery());
}

// 关键词全文搜索(title, description, content)
if (StringUtils.isNotBlank(searchText)) {
BoolQuery.Builder shouldBuilder = new BoolQuery.Builder();
shouldBuilder.should(MatchQuery.of(m -> m.field("title").query(searchText))._toQuery());
shouldBuilder.should(MatchQuery.of(m -> m.field("description").query(searchText))._toQuery());
shouldBuilder.should(MatchQuery.of(m -> m.field("content").query(searchText))._toQuery());
shouldBuilder.minimumShouldMatch("1");
boolQueryBuilder.should(shouldBuilder.build()._toQuery());
}

// 单独按 title 搜索
if (StringUtils.isNotBlank(title)) {
boolQueryBuilder.should(MatchQuery.of(m -> m.field("title").query(title))._toQuery());
}

// 单独按 content 搜索
if (StringUtils.isNotBlank(content)) {
boolQueryBuilder.should(MatchQuery.of(m -> m.field("content").query(content))._toQuery());
}

// 设置 minimum_should_match 仅在有 should 时生效
if (StringUtils.isNotBlank(searchText) || StringUtils.isNotBlank(title) || StringUtils.isNotBlank(content)) {
boolQueryBuilder.minimumShouldMatch("1");
}

// 构建排序
co.elastic.clients.elasticsearch._types.SortOptions sortOption;
if (StringUtils.isNotBlank(sortField)) {
sortOption = co.elastic.clients.elasticsearch._types.SortOptions.of(s ->
s.field(f -> f.field(sortField)
.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.Asc : SortOrder.Desc))
);
} else {
// 默认按 _score 降序(相关性)
sortOption = co.elastic.clients.elasticsearch._types.SortOptions.of(s -> s.score(ScoreSort.of(sc -> sc.order(SortOrder.Desc))));
}

// 分页
int from = (int) (current * pageSize);
int size = (int) pageSize;

SearchRequest searchRequest = SearchRequest.of(s -> s
.index("post") // 请确保索引名正确,建议从配置注入
.query(boolQueryBuilder.build()._toQuery())
.from(from)
.size(size)
.sort(sortOption)
);

SearchResponse<PostEsDTO> response;
try {
response = elasticsearchClient.search(searchRequest, PostEsDTO.class);
} catch (IOException e) {
log.error("Elasticsearch 查询失败", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "搜索服务异常");
}

Page<Post> page = new Page<>();
long total = response.hits().total().value();
page.setTotal(total);

List<Post> resourceList = new ArrayList<>();
if (!response.hits().hits().isEmpty()) {
List<Long> postIdList = response.hits().hits().stream()
.map(Hit::source)
.map(PostEsDTO::getId)
.collect(Collectors.toList());

// 从数据库获取最新数据(如点赞数)
List<Post> postList = baseMapper.selectBatchIds(postIdList);
if (postList != null) {
Map<Long, Post> idPostMap = postList.stream()
.collect(Collectors.toMap(Post::getId, p -> p));

for (Long postId : postIdList) {
Post post = idPostMap.get(postId);
if (post != null) {
resourceList.add(post);
} else {
// ES 中存在但 DB 已删除,清理 ES 数据
try {
elasticsearchClient.delete(d -> d.index("post").id(String.valueOf(postId)));
log.info("清理已删除帖子 ES 数据: {}", postId);
} catch (IOException ex) {
log.warn("清理 ES 数据失败: {}", postId, ex);
}
}
}
}
}

page.setRecords(resourceList);
page.setCurrent(postQueryRequest.getCurrent());
page.setSize(pageSize);
return page;
}

// ====== 以下方法保持不变(VO 转换等) ======

@Override
public PostVO getPostVO(Post post, HttpServletRequest request) {
PostVO postVO = PostVO.objToVo(post);
long postId = post.getId();
Long userId = post.getUser_id();
User user = null;
if (userId != null && userId > 0) {
user = userService.getById(userId);
}
UserVO userVO = userService.getUserVO(user);
postVO.setUser(userVO);

User loginUser = userService.getLoginUserPermitNull(request);
if (loginUser != null) {
QueryWrapper<PostThumb> thumbQw = new QueryWrapper<>();
thumbQw.eq("postId", postId).eq("userId", loginUser.getId());
PostThumb thumb = postThumbMapper.selectOne(thumbQw);
postVO.setHasThumb(thumb != null);

QueryWrapper<PostFavour> favQw = new QueryWrapper<>();
favQw.eq("postId", postId).eq("userId", loginUser.getId());
PostFavour favour = postFavourMapper.selectOne(favQw);
postVO.setHasFavour(favour != null);
}
return postVO;
}

@Override
public Page<PostVO> getPostVOPage(Page<Post> postPage, HttpServletRequest request) {
List<Post> postList = postPage.getRecords();
Page<PostVO> postVOPage = new Page<>(postPage.getCurrent(), postPage.getSize(), postPage.getTotal());
if (CollUtil.isEmpty(postList)) {
return postVOPage;
}

Set<Long> userIdSet = postList.stream().map(Post::getUser_id).collect(Collectors.toSet());
Map<Long, User> userMap = userService.listByIds(userIdSet).stream()
.collect(Collectors.toMap(User::getId, u -> u));

Map<Long, Boolean> postIdHasThumbMap = new HashMap<>();
Map<Long, Boolean> postIdHasFavourMap = new HashMap<>();

User loginUser = userService.getLoginUserPermitNull(request);
if (loginUser != null) {
Set<Long> postIdSet = postList.stream().map(Post::getId).collect(Collectors.toSet());
loginUser = userService.getLoginUser(request);

QueryWrapper<PostThumb> thumbQw = new QueryWrapper<>();
thumbQw.in("postId", postIdSet).eq("userId", loginUser.getId());
List<PostThumb> thumbs = postThumbMapper.selectList(thumbQw);
thumbs.forEach(t -> postIdHasThumbMap.put(t.getPost_id(), true));

QueryWrapper<PostFavour> favQw = new QueryWrapper<>();
favQw.in("postId", postIdSet).eq("userId", loginUser.getId());
List<PostFavour> favs = postFavourMapper.selectList(favQw);
favs.forEach(f -> postIdHasFavourMap.put(f.getPost_id(), true));
}

List<PostVO> postVOList = postList.stream().map(post -> {
PostVO vo = PostVO.objToVo(post);
vo.setUser(userService.getUserVO(userMap.get(post.getUser_id())));
vo.setHasThumb(postIdHasThumbMap.getOrDefault(post.getId(), false));
vo.setHasFavour(postIdHasFavourMap.getOrDefault(post.getId(), false));
return vo;
}).collect(Collectors.toList());

postVOPage.setRecords(postVOList);
return postVOPage;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 帖子点赞服务实现
*/
@Service
public class PostThumbServiceImpl extends ServiceImpl<PostThumbMapper, PostThumb>
implements PostThumbService {

@Resource
private PostService postService;

/**
* 点赞
*
* @param postId
* @param loginUser
* @return
*/
@Override
public int doPostThumb(long postId, User loginUser) {
// 判断实体是否存在,根据类别获取实体
Post post = postService.getById(postId);
if (post == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 是否已点赞
long userId = loginUser.getId();
// 每个用户串行点赞
// 锁必须要包裹住事务方法
PostThumbService postThumbService = (PostThumbService) AopContext.currentProxy();
synchronized (String.valueOf(userId).intern()) {
return postThumbService.doPostThumbInner(userId, postId);
}
}

/**
* 封装了事务的方法
*
* @param userId
* @param postId
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int doPostThumbInner(long userId, long postId) {
PostThumb postThumb = new PostThumb();
postThumb.setUser_id(userId);
postThumb.setPost_id(postId);
QueryWrapper<PostThumb> thumbQueryWrapper = new QueryWrapper<>(postThumb);
PostThumb oldPostThumb = this.getOne(thumbQueryWrapper);
boolean result;
// 已点赞
if (oldPostThumb != null) {
result = this.remove(thumbQueryWrapper);
if (result) {
// 点赞数 - 1
result = postService.update()
.eq("id", postId)
.gt("thumbNum", 0)
.setSql("thumbNum = thumbNum - 1")
.update();
return result ? -1 : 0;
} else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
} else {
// 未点赞
result = this.save(postThumb);
if (result) {
// 点赞数 + 1
result = postService.update()
.eq("id", postId)
.setSql("thumbNum = thumbNum + 1")
.update();
return result ? 1 : 0;
} else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/**
* 用户服务实现
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

/**
* 盐值,混淆密码
*/
public static final String SALT = "yupi";

@Override
public long userRegister(String userAccount, String userPassword, String checkPassword) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
}
// 密码和校验密码相同
if (!userPassword.equals(checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
}
synchronized (userAccount.intern()) {
// 账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = this.baseMapper.selectCount(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 3. 插入数据
User user = new User();
user.setUser_account(userAccount);
user.setUser_password(encryptPassword);
boolean saveResult = this.save(user);
if (!saveResult) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
}
return user.getId();
}
}

@Override
public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");
}
if (userPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 查询用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
queryWrapper.eq("userPassword", encryptPassword);
User user = this.baseMapper.selectOne(queryWrapper);
// 用户不存在
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
}
// 3. 记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE, user);
return this.getLoginUserVO(user);
}

@Override
public LoginUserVO userLoginByMpOpen(WxOAuth2UserInfo wxOAuth2UserInfo, HttpServletRequest request) {
String unionId = wxOAuth2UserInfo.getUnionId();
String mpOpenId = wxOAuth2UserInfo.getOpenid();
// 单机锁
synchronized (unionId.intern()) {
// 查询用户是否已存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("unionId", unionId);
User user = this.getOne(queryWrapper);
// 被封号,禁止登录
if (user != null && UserRoleEnum.BAN.getValue().equals(user.getUser_role())) {
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "该用户已被封,禁止登录");
}
// 用户不存在则创建
if (user == null) {
user = new User();
user.setUnion_id(unionId);
user.setMpOpen_id(mpOpenId);
user.setUser_avatar(wxOAuth2UserInfo.getHeadImgUrl());
user.setUser_name(wxOAuth2UserInfo.getNickname());
boolean result = this.save(user);
if (!result) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败");
}
}
// 记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE, user);
return getLoginUserVO(user);
}
}

/**
* 获取当前登录用户
*
* @param request
* @return
*/
@Override
public User getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null || currentUser.getId() == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
// 从数据库查询(追求性能的话可以注释,直接走缓存)
long userId = currentUser.getId();
currentUser = this.getById(userId);
if (currentUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
return currentUser;
}

/**
* 获取当前登录用户(允许未登录)
*
* @param request
* @return
*/
@Override
public User getLoginUserPermitNull(HttpServletRequest request) {
// 先判断是否已登录
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null || currentUser.getId() == null) {
return null;
}
// 从数据库查询(追求性能的话可以注释,直接走缓存)
long userId = currentUser.getId();
return this.getById(userId);
}

/**
* 是否为管理员
*
* @param request
* @return
*/
@Override
public boolean isAdmin(HttpServletRequest request) {
// 仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userObj;
return isAdmin(user);
}

@Override
public boolean isAdmin(User user) {
return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUser_role());
}

/**
* 用户注销
*
* @param request
*/
@Override
public boolean userLogout(HttpServletRequest request) {
if (request.getSession().getAttribute(USER_LOGIN_STATE) == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
}
// 移除登录态
request.getSession().removeAttribute(USER_LOGIN_STATE);
return true;
}

@Override
public LoginUserVO getLoginUserVO(User user) {
if (user == null) {
return null;
}
LoginUserVO loginUserVO = new LoginUserVO();
BeanUtils.copyProperties(user, loginUserVO);
return loginUserVO;
}

@Override
public UserVO getUserVO(User user) {
if (user == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return userVO;
}

@Override
public List<UserVO> getUserVO(List<User> userList) {
if (CollUtil.isEmpty(userList)) {
return new ArrayList<>();
}
return userList.stream().map(this::getUserVO).collect(Collectors.toList());
}

@Override
public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {
if (userQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
}
Long id = userQueryRequest.getId();
String unionId = userQueryRequest.getUnionId();
String mpOpenId = userQueryRequest.getMpOpenId();
String userName = userQueryRequest.getUserName();
String userProfile = userQueryRequest.getUserProfile();
String userRole = userQueryRequest.getUserRole();
String sortField = userQueryRequest.getSortField();
String sortOrder = userQueryRequest.getSortOrder();
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(id != null, "id", id);
queryWrapper.eq(StringUtils.isNotBlank(unionId), "unionId", unionId);
queryWrapper.eq(StringUtils.isNotBlank(mpOpenId), "mpOpenId", mpOpenId);
queryWrapper.eq(StringUtils.isNotBlank(userRole), "userRole", userRole);
queryWrapper.like(StringUtils.isNotBlank(userProfile), "userProfile", userProfile);
queryWrapper.like(StringUtils.isNotBlank(userName), "userName", userName);
queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
sortField);
return queryWrapper;
}
}

工具类 utils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 网络工具类
*/
public class NetUtils {

/**
* 获取客户端 IP 地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
// 根据网卡取本机配置的 IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
if (inet != null) {
ip = inet.getHostAddress();
}
}
}
// 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
if (ip == null) {
return "127.0.0.1";
}
return ip;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Spring 上下文获取工具
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}

/**
* 通过名称获取 Bean
*
* @param beanName
* @return
*/
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}

/**
* 通过 class 获取 Bean
*
* @param beanClass
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> beanClass) {
return applicationContext.getBean(beanClass);
}

/**
* 通过名称和类型获取 Bean
*
* @param beanName
* @param beanClass
* @param <T>
* @return
*/
public static <T> T getBean(String beanName, Class<T> beanClass) {
return applicationContext.getBean(beanName, beanClass);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* SQL 工具
*/
public class SqlUtils {

/**
* 校验排序字段是否合法(防止 SQL 注入)
*
* @param sortField
* @return
*/
public static boolean validSortField(String sortField) {
if (StringUtils.isBlank(sortField)) {
return false;
}
return !StringUtils.containsAny(sortField, "=", "(", ")", " ");
}
}

微信模块 wxmp

1
2
3
4
5
6
7
8
9
/**
* 微信公众号相关常量
**/
public class WxMpConstant {
/**
* 点击菜单 key
*/
public static final String CLICK_MENU_KEY = "CLICK_MENU_KEY";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 微信公众号路由
*/
@Configuration
public class WxMpMsgRouter {

@Resource
private WxMpService wxMpService;

@Resource
private EventHandler eventHandler;

@Resource
private MessageHandler messageHandler;

@Resource
private SubscribeHandler subscribeHandler;

@Bean
public WxMpMessageRouter getWxMsgRouter() {
WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
// 消息
router.rule()
.async(false)
.msgType(XmlMsgType.TEXT)
.handler(messageHandler)
.end();
// 关注
router.rule()
.async(false)
.msgType(XmlMsgType.EVENT)
.event(EventType.SUBSCRIBE)
.handler(subscribeHandler)
.end();
// 点击按钮
router.rule()
.async(false)
.msgType(XmlMsgType.EVENT)
.event(EventType.CLICK)
.eventKey(WxMpConstant.CLICK_MENU_KEY)
.handler(eventHandler)
.end();
return router;
}
}

处理器 handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 事件处理器
**/
@Component
public class EventHandler implements WxMpMessageHandler {

@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService,
WxSessionManager wxSessionManager) throws WxErrorException {
final String content = "您点击了菜单";
// 调用接口,返回验证码
return WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 消息处理器
**/
@Component
public class MessageHandler implements WxMpMessageHandler {

@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
String content = "我是复读机:" + wxMpXmlMessage.getContent();
return WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 关注处理器
*/
@Component
public class SubscribeHandler implements WxMpMessageHandler {

@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
final String content = "感谢关注";
// 调用接口,返回验证码
return WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
}