#
zhou zhou
1 天以前 4259deb19122a4807d50c99ed4a95405ebe4a47c
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
291
292
293
294
295
296
297
298
299
300
301
302
303
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;
    }
 
}