package com.vincent.rsf.server.system.controller;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.vincent.rsf.common.utils.Utils;
|
import com.vincent.rsf.framework.common.Cools;
|
import com.vincent.rsf.server.common.domain.BaseParam;
|
import com.vincent.rsf.server.common.domain.CursorPageParam;
|
import com.vincent.rsf.server.common.domain.CursorPageResult;
|
import com.vincent.rsf.server.common.domain.PageParam;
|
import com.vincent.rsf.server.system.entity.User;
|
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
import java.lang.reflect.Field;
|
import java.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.function.Consumer;
|
|
/**
|
* Created by vincent on 1/30/2024
|
*/
|
|
public class BaseController {
|
public User getLoginUser() {
|
try {
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
if (authentication != null) {
|
Object object = authentication.getPrincipal();
|
if (object instanceof User) {
|
return (User) object;
|
}
|
}
|
} catch (Exception e) {
|
System.out.println(e.getMessage());
|
}
|
return null;
|
}
|
|
public Long getLoginUserId() {
|
User loginUser = getLoginUser();
|
return loginUser == null ? null : loginUser.getId();
|
}
|
|
public Long getTenantId() {
|
User loginUser = getLoginUser();
|
return loginUser == null ? null : loginUser.getTenantId();
|
}
|
|
public boolean hasAuthority(String authority) {
|
if (authority == null || authority.trim().isEmpty()) {
|
return false;
|
}
|
try {
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
if (authentication == null || authentication.getAuthorities() == null) {
|
return false;
|
}
|
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
|
if (grantedAuthority != null && authority.equals(grantedAuthority.getAuthority())) {
|
return true;
|
}
|
}
|
} catch (Exception e) {
|
System.out.println(e.getMessage());
|
}
|
return false;
|
}
|
|
public <T extends BaseParam> T buildParam(Map<String, Object> map, Class<T> clz) {
|
if (!Objects.isNull(map.get("meta"))) {
|
Map<String, Object> meta = (Map<String, Object>) map.get("meta");
|
meta.keySet().forEach(key -> {
|
map.put(key, meta.get(key));
|
});
|
map.remove("meta");
|
}
|
// 移除筛选条件里面的 $
|
for (String key : map.keySet()) {
|
Object value = map.get(key);
|
if (key.equals("orderBy")) {
|
String newValue = value.toString().replace("$", "");
|
map.replace("orderBy", value, newValue);
|
}
|
}
|
T t = null;
|
try {
|
t = clz.getDeclaredConstructor().newInstance();
|
t.syncMap(map);
|
} catch (Exception e) {
|
e.printStackTrace();
|
}
|
return t;
|
}
|
|
public Boolean hasCreateTimeDesc(List<OrderItem> orderItems) {
|
if (Cools.isEmpty(orderItems)) {
|
return true;
|
}
|
if (orderItems.size() > 1) {
|
return false;
|
}
|
OrderItem orderItem = orderItems.get(0);
|
if (!orderItem.getColumn().equals("create_time")) {
|
return false;
|
}
|
if (orderItem.isAsc()) {
|
return false;
|
}
|
return true;
|
}
|
|
/**
|
* 通用游标分页实现。
|
*
|
* <p>这个方法的目标不是替代所有分页,而是把“单字段、倒序、向后翻页”的游标分页
|
* 收敛成一套统一实现,避免各个 controller 重复写下面这些样板逻辑:</p>
|
* <ul>
|
* <li>buildParam</li>
|
* <li>忽略前端传入的 orderBy</li>
|
* <li>buildWrapper(true) 构建通用筛选</li>
|
* <li>cursor 条件</li>
|
* <li>按固定字段倒序</li>
|
* <li>多查一条判断 hasNext</li>
|
* <li>截断结果并生成 nextCursor</li>
|
* </ul>
|
*
|
* <p>适用前提:</p>
|
* <ul>
|
* <li>游标字段是单一字段</li>
|
* <li>游标字段的值稳定、可比较,并且能映射成 Long</li>
|
* <li>分页方向固定为“按该字段倒序,向更小的值翻页”</li>
|
* </ul>
|
*
|
* <p>参数说明:</p>
|
* <ul>
|
* <li>{@code map}:原始请求参数</li>
|
* <li>{@code paramClass}:游标参数类型,通常传 {@link CursorPageParam}</li>
|
* <li>{@code entityClass}:实体类,用于 PageParam 条件构建和 condition 模糊查询</li>
|
* <li>{@code service}:MyBatis-Plus 的 IService,负责执行 page 查询</li>
|
* <li>{@code cursorField}:实体字段名,不是数据库列名,例如传 {@code id}</li>
|
* <li>{@code defaultPageSize}:当前接口的默认分页大小</li>
|
* <li>{@code wrapperConsumer}:可选的额外 where 条件扩展钩子</li>
|
* <li>{@code recordsConsumer}:可选的结果后处理钩子,例如补充 createBy$/updateBy$</li>
|
* </ul>
|
*/
|
protected <T, U extends CursorPageParam> CursorPageResult<T> cursorPage(
|
Map<String, Object> map,
|
Class<U> paramClass,
|
Class<T> entityClass,
|
IService<T> service,
|
String cursorField,
|
int defaultPageSize,
|
Consumer<QueryWrapper<T>> wrapperConsumer,
|
Consumer<List<T>> recordsConsumer
|
) {
|
// 允许 controller 传 null,内部统一兜底成空 map,
|
// 这样 buildParam 不需要每个调用方自己先判空。
|
U baseParam = buildParam(map == null ? new HashMap<>() : map, paramClass);
|
|
// 游标分页不允许客户端自定义排序,
|
// 否则“上一页最后一条作为下一页游标”的前提会被破坏。
|
baseParam.setOrderBy(null);
|
|
// pageSize 允许从请求里带入,但非法值(null、0、负数)统一回退到接口默认值。
|
int pageSize = resolveCursorPageSize(baseParam.getPageSize(), defaultPageSize);
|
|
// controller 传的是实体字段名,例如 "id" / "poId",
|
// 这里统一转成数据库列名并补反引号,避免每个业务自己手写 SQL 片段。
|
String cursorColumn = resolveCursorColumn(cursorField);
|
|
// 先复用系统现有的 PageParam + buildWrapper(true) 机制,
|
// 保留原来的条件解析、时间范围、condition 模糊搜索等能力。
|
PageParam<T, U> pageParam = new PageParam<>(baseParam, entityClass);
|
QueryWrapper<T> wrapper = pageParam.buildWrapper(true);
|
|
// 给业务预留额外 where 条件的扩展点;
|
// 如果某个接口除了通用筛选外,还要拼接额外限制,可以在这里补。
|
if (wrapperConsumer != null) {
|
wrapperConsumer.accept(wrapper);
|
}
|
|
// 游标分页的核心条件:
|
// 当前约定是“按 cursorField 倒序查看更旧的数据”,所以条件固定为 < cursor。
|
if (baseParam.getCursor() != null) {
|
wrapper.lt(cursorColumn, baseParam.getCursor());
|
}
|
|
// 强制按游标字段倒序排序,保证每一页的数据顺序稳定。
|
wrapper.orderByDesc(cursorColumn);
|
|
// 多查一条是游标分页判断 hasNext 的常见做法:
|
// 实际要 20 条,就查 21 条;多出来那一条只用来判断是否还有下一页。
|
Page<T> queryPage = new Page<>(1L, pageSize + 1L, false);
|
List<T> records = service.page(queryPage, wrapper).getRecords();
|
List<T> pageRecords = Cools.isEmpty(records) ? new ArrayList<>() : new ArrayList<>(records);
|
|
// 如果查出来的数量大于 pageSize,说明至少还有下一页。
|
boolean hasNext = pageRecords.size() > pageSize;
|
if (hasNext) {
|
// 只把真正需要返回给前端的 pageSize 条数据留在当前页。
|
pageRecords = new ArrayList<>(pageRecords.subList(0, pageSize));
|
}
|
|
// 给业务侧一个“结果出库前处理”的机会。
|
// 典型场景是批量补充用户名、字典文本、缓存字段等,
|
// 这样公共分页逻辑不关心业务细节,但业务也不需要回到 controller 自己重写分页。
|
if (recordsConsumer != null && !Cools.isEmpty(pageRecords)) {
|
recordsConsumer.accept(pageRecords);
|
}
|
|
CursorPageResult<T> result = new CursorPageResult<>();
|
result.setRecords(pageRecords);
|
result.setPageSize(pageSize);
|
result.setHasNext(hasNext);
|
// nextCursor 只有在还有下一页时才有意义;
|
// 约定取“当前页最后一条记录”的游标字段值。
|
result.setNextCursor(hasNext ? extractCursorValue(pageRecords, cursorField) : null);
|
return result;
|
}
|
|
/**
|
* 统一解析当前接口实际使用的 pageSize。
|
*
|
* <p>只要前端没传、传了 0、或者传了负数,就回退到 controller 传入的默认值。</p>
|
*/
|
private int resolveCursorPageSize(Integer pageSize, int defaultPageSize) {
|
if (pageSize == null || pageSize <= 0) {
|
return defaultPageSize;
|
}
|
return pageSize;
|
}
|
|
/**
|
* 把实体字段名转换成数据库列名。
|
*
|
* <p>例如:</p>
|
* <ul>
|
* <li>{@code id -> `id`}</li>
|
* <li>{@code poId -> `po_id`}</li>
|
* </ul>
|
*
|
* <p>这样 controller 调用时只需要关心 Java 字段名,不需要自己拼 SQL。</p>
|
*/
|
private String resolveCursorColumn(String cursorField) {
|
return "`" + Utils.toSymbolCase(cursorField, '_') + "`";
|
}
|
|
/**
|
* 从当前页最后一条记录中提取 nextCursor。
|
*
|
* <p>这里使用反射而不是额外定义接口,目的是降低接入成本:
|
* 只要实体里存在同名字段,就能直接复用通用方法。</p>
|
*
|
* <p>支持的字段值类型:</p>
|
* <ul>
|
* <li>{@link Long}</li>
|
* <li>其他 {@link Number}</li>
|
* <li>可转成 Long 的字符串</li>
|
* </ul>
|
*
|
* <p>如果字段不存在、为空、或无法转成 Long,则返回 null。</p>
|
*/
|
private <T> Long extractCursorValue(List<T> records, String cursorField) {
|
if (Cools.isEmpty(records)) {
|
return null;
|
}
|
T lastRecord = records.get(records.size() - 1);
|
if (lastRecord == null || Cools.isEmpty(cursorField)) {
|
return null;
|
}
|
Field field = Cools.getField(lastRecord.getClass(), cursorField);
|
if (field == null) {
|
return null;
|
}
|
boolean accessible = field.isAccessible();
|
try {
|
field.setAccessible(true);
|
Object value = field.get(lastRecord);
|
if (value instanceof Long) {
|
return (Long) value;
|
}
|
if (value instanceof Number) {
|
return ((Number) value).longValue();
|
}
|
if (value instanceof String && !((String) value).trim().isEmpty()) {
|
return Long.parseLong(((String) value).trim());
|
}
|
} catch (IllegalAccessException | NumberFormatException ignored) {
|
return null;
|
} finally {
|
field.setAccessible(accessible);
|
}
|
return null;
|
}
|
|
}
|