feat(constool): 添加数据对比同步工具并重构动态表格构建器

- 新增 KitDiffSyncConstTool 工具类,支持集合数据的对比同步功能
- 添加数据同步回调接口和完整的增删改查批量操作支持
- 重命名 KitDynamicTableUtil 为 KitDynamicTableBuilder 并调整方法名称
- 修改 renderOrder 方法为 ofOrder,renderAll 方法为 ofAll
- 更新前端 FormatDuration 函数,支持年、天、时、分、秒格式化显示
- 添加字节格式化方法 renderBytes 到 KitStringUtil 工具类
- 在首页视图中集成 Pipeline 组件用于展示任务流程
- 添加窗口打开工具函数 OpenWindowInCurrent 和 OpenWindowInNew
- 创建 MybatisMapperBatchOpRender 批量操作生成器
This commit is contained in:
2026-03-06 19:19:34 +08:00
parent afbdbbdea8
commit a14809e203
22 changed files with 1749 additions and 275 deletions
+8
View File
@@ -273,3 +273,11 @@ export function IsNotBlank(str) {
export function IsBlank(str) { export function IsBlank(str) {
return str == null || String(str).trim().length === 0 return str == null || String(str).trim().length === 0
} }
export function OpenWindowInCurrent(url) {
window.location.href = url
}
export function OpenWindowInNew(url) {
window.open(url, '_blank')
}
+16 -7
View File
@@ -390,22 +390,31 @@ export function FormatEnabled(value) {
*/ */
export function FormatDuration(value) { export function FormatDuration(value) {
if (value === null || value === undefined || value === '') { if (value === null || value === undefined || value === '') {
return '-' return ''
} }
const seconds = Number(value) const seconds = Number(value / 1000)
if (isNaN(seconds)) { if (isNaN(seconds)) {
return '-' return ''
} }
const hours = Math.floor(seconds / 3600) const years = Math.floor(seconds / (365 * 24 * 3600))
const days = Math.floor((seconds % (365 * 24 * 3600)) / (24 * 3600))
const hours = Math.floor((seconds % (24 * 3600)) / 3600)
const minutes = Math.floor((seconds % 3600) / 60) const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60 const secs = (seconds % 60).toFixed(0)
if (hours > 0) { if (years > 0) {
return `${hours}小时${minutes}分${secs}秒` return `${years}年${days}天${hours}时${minutes}分${secs}秒`
} else if (days > 0) {
return `${days}天${hours}时${minutes}分${secs}秒`
} else if (hours > 0) {
return `${hours}时${minutes}分${secs}秒`
} else if (minutes > 0) { } else if (minutes > 0) {
return `${minutes}分${secs}秒` return `${minutes}分${secs}秒`
} else { } else {
if (secs === '0') {
return ``
}
return `${secs}秒` return `${secs}秒`
} }
} }
@@ -0,0 +1,317 @@
<template>
<div :style="taskWindowStyle">
<div :style="taskHeaderStyle" />
<div :style="taskBodyStyle">
<!-- 标题和图标 -->
<el-row :gutter="2">
<el-col :span="16">{{ name }}</el-col>
<el-col :span="8" style="text-align: right">
<i :class="getStatusIconClassByStatus(status)" :style="getStatusIconColorByStatus(status)" />
</el-col>
</el-row>
<!-- 分割线 -->
<div style="padding-left: 10px; padding-right: 10px; padding-top: 2px; border-bottom: 1px solid #EBEEF5" />
<!-- 任务执行信息 -->
<div
v-if="click === false"
style="width: 100%; height: 40px; line-height: 30px; font-size: 12px; color: #606266;
text-align: center;padding: 9px 0;font-family: Arial, sans-serif"
>
{{ desc }}
</div>
<el-button
v-else
v-prevent-re-click
type="text"
style="width: 100%; height: 40px; line-height: 30px; font-size: 12px; color: #606266"
@click="onTaskDetailClick"
>
{{ desc }}
</el-button>
<el-row :gutter="2">
<!-- 持续时间 -->
<el-col :span="15" style="font-size: 12px;color: #606266;height: 32px;max-height: 32px;line-height: 32px">
{{ FormatDuration(status !== statusMap.running ? durationMillis : new Date().getTime() - startTimeMillis) }}
</el-col>
<!-- 任务失败,且支持重试 -->
<el-col v-if="status === statusMap.failed && retry === true" :span="9" style="font-size: 12px;color: #606266;height: 32px;max-height: 32px">
<el-button v-prevent-re-click type="text" @click="onRetryClick">重试</el-button>
</el-col>
<!-- 任务运行中,且包含按钮组 -->
<el-col v-if="buttonList && buttonList.length > 0" :span="9" style="font-size: 12px;color: #606266;height: 32px;max-height: 32px;padding-right: 5px">
<el-button
v-for="(item, index) in buttonList"
:key="index"
v-prevent-re-click
type="text"
:style="`width: ${item.text && item.text.length >= 4 ? '74px': '26px'
};color: ${item.color ? item.color : statusColorMap.running};display: ${item.condition ===
'all' ?
'inline-block':
(item.condition === status ? 'inline-block': 'none')};overflow: hidden;text-overflow: ellipsis;`"
@click="onButtonNodeClick(item)"
>{{ item.text }}</el-button>
</el-col>
</el-row>
</div>
<!-- 任务节点明细 -->
<cute-preview-drawer ref="taskDetailDrawer" :title="name">
<el-timeline>
<el-timeline-item
v-for="(taskDetail, index) in taskDetails"
:key="index"
:color="getColorHexByStatus(taskDetail.status)"
:timestamp="taskDetail.timestamp"
>
{{ taskDetail.content }}
</el-timeline-item>
</el-timeline>
</cute-preview-drawer>
</div>
</template>
<script>
import KitMessage from '@/utils/elementui/KitMessage'
import CutePreviewDrawer from '@/views/components/dev/CutePreviewDrawer.vue'
import { OpenWindowInCurrent, OpenWindowInNew } from '@/utils/KitUtil'
import request from '@/utils/request'
import { FormatDuration } from '@/utils/filters'
export default {
name: 'PipeTask',
components: { CutePreviewDrawer },
props: {
/**
* 任务名称
*/
name: {
type: String,
required: true
},
/**
* 任务编码
*/
code: {
type: String,
required: true
},
/**
* 开始时间戳。单位:毫秒
*/
startTimeMillis: {
type: Number,
required: true
},
/**
* 持续时间。单位:秒
*/
durationMillis: {
type: Number,
required: true
},
/**
* 状态
*/
status: {
type: String,
required: true
},
/**
* 状态(翻译)
*/
statusDesc: {
type: String,
required: true
},
/**
* 任务执行信息
*/
desc: {
type: String,
required: true
},
/**
* 是否可以点击
*/
click: {
type: Boolean,
required: false,
default: false
},
/**
* 是否可以重试
*/
retry: {
type: Boolean,
required: false,
default: false
},
/**
* 任务明细类型(gitlab、jenkins等)
*/
detailType: {
type: String,
required: false,
default: null
},
/**
* 按钮组
* @property type: execute(请求系统api)、link(调用三方链接跳转)
* @property text: 按钮标题
* @property color: 按钮颜色
* @property condition: 符合条件才展示,当值为`all`时,非pending状态都展示
* @property method: 当type=execute时,配置请求方式:GET/POST
* @property requestUrl: 当type=execute时,配置请求路径和参数
* @property linkUrl: 当type=link时,配置跳转路径和参数
* @property isBlank: 当type=link时,判断是否在新页面中打开
*/
buttonList: {
type: Array,
required: false,
default: function() {
return []
}
}
},
data() {
return {
statusMap: {
pending: 'pending',
running: 'running',
success: 'success',
failed: 'failed'
},
statusColorMap: {
pending: '#EBEEF5',
success: '#67C23A',
failed: '#F56C6C',
running: '#409EFF'
},
// 任务明细
taskDetails: [
// { timestamp: '2018-04-03 20:46', content: '开始初始化', status: 'success' },
// { timestamp: '2018-04-03 20:46', content: '开始初始化', status: 'success' },
// { timestamp: '2018-04-03 20:46', content: '开始初始化', status: 'failed' }
]
}
},
computed: {
taskWindowStyle() {
const colorHex = this.getColorHexByStatus(this.status)
return {
width: '240px',
maxWidth: '240px',
height: '100px',
maxHeight: '100px',
border: `1px solid ${colorHex}`,
borderRadius: '6px',
overflow: 'hidden',
display: 'inline-block'
}
},
taskHeaderStyle() {
const colorHex = this.getColorHexByStatus(this.status)
return {
width: '100%',
height: '5px',
backgroundColor: colorHex
}
},
taskBodyStyle() {
return {
padding: '5px',
fontSize: '14px',
backgroundColor: '#FFFFFF',
color: '#606266',
height: '95px'
}
}
},
watch: {},
mounted() {
},
methods: {
FormatDuration,
/**
* 根据任务状态获取颜色Hex值
*/
getColorHexByStatus(status) {
const colorHex = this.statusColorMap[status]
if (colorHex) {
return colorHex
}
return this.statusColorMap.pending
},
/**
* 根据任务状态渲染状态图标样式
* @param status
*/
getStatusIconClassByStatus(status) {
if (this.statusMap.success === status) {
return 'el-icon-circle-check'
}
if (this.statusMap.failed === status) {
return 'el-icon-circle-close'
}
if (this.statusMap.running === status) {
return 'el-icon-loading'
}
return 'el-icon-time'
},
/**
* 根据任务状态渲染状态图标颜色
* @param status
*/
getStatusIconColorByStatus(status) {
const styles = {
width: '14px',
height: '14px'
}
styles.color = this.getColorHexByStatus(status)
return styles
},
onTaskDetailClick() {
console.error('当任务卡片可点击时,查看明细记录')
if (this.statusMap.pending === this.status) {
KitMessage.Error('节点任务未开始')
return
}
if (this.detailType === 'gitlab') {
// 获取gitlab任务明细
} else if (this.detailType === 'jenkins') {
// 获取jenkins任务明细
} else {
// 获取业务服务明细
}
this.$refs.taskDetailDrawer.show()
},
onRetryClick() {
console.error('当任务卡片可重试时,任务重试', this.code)
this.$emit('retry', this.code)
},
async onButtonNodeClick(buttonProps) {
console.error('当任务卡片包含按钮组时,某个按钮被单击', buttonProps)
if (buttonProps.type === 'execute') {
const { method, requestUrl } = buttonProps
if (method === 'get' || method === 'post') {
const response = await request({ url: requestUrl, method: method })
console.error('请求系统内置接口结果:', response)
} else {
KitMessage.Error(`'${buttonProps.text}' 按钮异常,不支持的请求方式`)
}
} else if (buttonProps.type === 'link') {
const { blank, linkUrl } = buttonProps
if (blank) {
OpenWindowInNew(linkUrl)
} else {
OpenWindowInCurrent(linkUrl)
}
} else {
KitMessage.Error(`'${buttonProps.text}' 按钮异常,不支持的类型`)
}
}
}
}
</script>
@@ -0,0 +1,70 @@
<template>
<cute-horizontal-scroll-bar :width="maxWidth">
<i v-for="(item, index) in taskNodes" :key="index">
<!-- 最后一个元素如何渲染 -->
<i v-if="index === taskNodes.length - 1">
<pipe-task v-bind="item" />
</i>
<!-- 其他元素如何渲染 -->
<i v-else class="pipeline-card">
<pipe-task v-bind="item" />
<div class="pipeline-link" />
</i>
</i>
</cute-horizontal-scroll-bar>
</template>
<script>
import PipeTask from '@/views/components/devops/pipeline/PipeTask.vue'
import CuteHorizontalScrollBar from '@/views/components/dev/CuteHorizontalScrollBar.vue'
export default {
name: 'Pipeline',
components: { CuteHorizontalScrollBar, PipeTask },
props: {
/**
* 流水线全局唯一实例ID
*/
instanceId: {
type: String,
required: true
},
/**
* 流水线任务节点明细
*/
taskNodes: {
type: Array,
required: true
}
},
data() {
return {}
},
computed: {
maxWidth() {
return `${document.body.clientWidth - 310}px`
}
},
watch: {},
mounted() {
},
methods: {}
}
</script>
<style lang="scss" scoped>
.pipeline-card {
width: 240px;
max-width: 240px;
}
.pipeline-link {
height: 3px;
width: 30px;
display: inline-block;
background-color: #E4E7ED;
margin-bottom: 3em;
margin-right: 4px;
}
</style>
+397 -2
View File
@@ -3,11 +3,20 @@
<github-corner class="github-corner" /> <github-corner class="github-corner" />
<el-row :gutter="40" style="background-color: white;margin-left: 2px;margin-right: 2px;padding: 6px"> <el-row :gutter="40" style="background-color: white;margin-left: 2px;margin-right: 2px;padding: 6px">
<el-col>系统API文档:<el-link type="primary" :href="swaggerApi" target="_blank">{{ swaggerApi }}</el-link></el-col> <el-col>系统API文档:
<el-link type="primary" :href="swaggerApi" target="_blank">{{ swaggerApi }}</el-link>
</el-col>
</el-row> </el-row>
<panel-group @handleSetLineChartData="handleSetLineChartData" /> <panel-group @handleSetLineChartData="handleSetLineChartData" />
<div style="border: 1px solid #E4E7ED;margin-bottom: 10px">
<Pipeline instance-id="instanceId1" :task-nodes="task1Nodes" />
<Pipeline instance-id="instanceId2" :task-nodes="task2Nodes" />
<Pipeline instance-id="instanceId3" :task-nodes="task3Nodes" />
<Pipeline instance-id="instanceId4" :task-nodes="task4Nodes" />
</div>
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;"> <el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData" /> <line-chart :chart-data="lineChartData" />
</el-row> </el-row>
@@ -39,6 +48,7 @@ import RadarChart from '@/components/Echarts/RadarChart'
import PieChart from '@/components/Echarts/PieChart' import PieChart from '@/components/Echarts/PieChart'
import BarChart from '@/components/Echarts/BarChart' import BarChart from '@/components/Echarts/BarChart'
import store from '@/store' import store from '@/store'
import Pipeline from '@/views/components/devops/pipeline/Pipeline.vue'
const lineChartData = { const lineChartData = {
newVisitis: { newVisitis: {
@@ -62,6 +72,7 @@ const lineChartData = {
export default { export default {
name: 'Dashboard', name: 'Dashboard',
components: { components: {
Pipeline,
GithubCorner, GithubCorner,
PanelGroup, PanelGroup,
LineChart, LineChart,
@@ -72,7 +83,391 @@ export default {
data() { data() {
return { return {
swaggerApi: store.getters.swaggerApi, swaggerApi: store.getters.swaggerApi,
lineChartData: lineChartData.newVisitis lineChartData: lineChartData.newVisitis,
task1Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'success',
statusDesc: '成功'
},
{
click: false,
code: 'stc_upload',
desc: '代码上传成功',
durationMillis: 16123,
name: 'STC扫描(代码上传)',
startTimeMillis: 1652343161022,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'build',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 104692,
name: '构建',
startTimeMillis: 1652343177145,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'all',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看扫描结果',
type: 'link'
}
],
click: false,
code: 'stc_result',
desc: 'STC扫描通过',
durationMillis: 18971,
name: 'STC扫描(扫描结果)',
startTimeMillis: 1652343281837,
status: 'success',
statusDesc: '成功'
},
{
click: false,
code: 'doPreCheck',
desc: '【田俊】确认通过',
durationMillis: 69937,
name: '发布前确认',
startTimeMillis: 1652343300808,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
}
],
click: false,
code: 'deploy',
desc: '成功',
durationMillis: 161975,
name: '部署',
startTimeMillis: 1652343370745,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 0,
name: '合并主干',
desc: '合并master确认',
startTimeMillis: 0,
status: 'running',
statusDesc: '执行中'
}
],
task2Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'build',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 104692,
name: '构建',
startTimeMillis: 1652343177145,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doAgree?id=60012',
text: '同意',
type: 'execute'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doRefuse?id=60012',
text: '拒绝',
color: '#ff0000',
type: 'execute'
}
],
click: false,
code: 'deploy',
desc: '等待部署审批',
durationMillis: 161975,
name: '部署',
startTimeMillis: 1652343370745,
status: 'running',
statusDesc: '执行中'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 7446,
name: '合并主干',
desc: '合并master确认',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
}
],
task3Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行失败',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'failed',
statusDesc: '失败'
},
{
click: true,
retry: true,
code: 'build',
desc: '待执行',
detailType: 'gitlab',
durationMillis: 0,
name: '构建',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doAgree?id=60012',
text: '同意',
type: 'execute'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doRefuse?id=60012',
text: '拒绝',
color: '#ff0000',
type: 'execute'
}
],
click: false,
code: 'deploy',
desc: '待执行',
durationMillis: 0,
name: '部署',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 0,
name: '合并主干',
desc: '待执行',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
}
],
task4Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'build',
desc: '执行失败',
detailType: 'gitlab',
durationMillis: 50331,
name: '构建',
startTimeMillis: 1652343110691,
status: 'failed',
statusDesc: '失败'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doAgree?id=60012',
text: '同意',
type: 'execute'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doRefuse?id=60012',
text: '拒绝',
color: '#ff0000',
type: 'execute'
}
],
click: false,
code: 'deploy',
desc: '待执行',
durationMillis: 0,
name: '部署',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 0,
name: '合并主干',
desc: '待执行',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
}
]
} }
}, },
mounted() { mounted() {
@@ -0,0 +1,168 @@
package cn.odboy.constool;
import cn.hutool.core.collection.CollUtil;
import cn.odboy.base.KitBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 数据对比同步工具
* <p>
* 适用场景:对比两个相同类型的对象集合,以对照组(compareRecords)为基准:
* <p>
* - 本地不存在的数据 → 新增
* <p>
* - 本地存在的数据 → 更新
* <p>
* - 对照组不存在的数据 → 删除
*
* @param <T> 数据类型
* @author odboy
*/
public class KitDiffSyncConstTool<T> {
/**
* 数据同步回调接口
*
* @param <T> 数据类型
*/
public interface SyncCallback<T> {
/**
* 全量存储对照组数据
*
* @param records 需要保存的数据列表
*/
void doSaveBatch(List<T> records);
/**
* 批量更新数据
*
* @param localList 本地已存在的数据列表
* @param compareList 对照组对应的数据列表
*/
void doUpdateBatch(List<T> localList, List<T> compareList);
/**
* 批量删除数据
*
* @param records 需要删除的数据列表
*/
void doDeleteBatch(List<T> records);
}
/**
* 本地数据源
*/
private final List<T> localRecords;
/**
* 对照数据源(以对照数据源为准)
*/
private final List<T> compareRecords;
/**
* 构造函数
*
* @param localRecords 本地数据源
* @param compareRecords 对照数据源(基准数据)
*/
public KitDiffSyncConstTool(List<T> localRecords, List<T> compareRecords) {
this.localRecords = localRecords;
this.compareRecords = compareRecords;
}
/**
* 执行数据对比同步
*
* @param keyMapper 唯一键映射函数,用于比较数据唯一性(如:Entity::getId)
* @param callback 同步回调接口,处理具体的增删改操作
* @param <K> 唯一键类型
*/
public <K> void sync(Function<? super T, ? extends K> keyMapper, SyncCallback<T> callback) {
if (keyMapper == null) {
throw new BadRequestException("参数 keyMapper 必填");
}
if (callback == null) {
throw new BadRequestException("参数 callback 必填");
}
// 如果本地数据为空,直接全量保存对照数据
if (this.localRecords.isEmpty()) {
callback.doSaveBatch(this.compareRecords);
return;
}
// 构建本地数据的键值映射
Map<K, T> localMap = localRecords.stream().collect(Collectors.toMap(keyMapper, i -> i));
List<T> insertList = new ArrayList<>();
List<T> updateCompareList = new ArrayList<>();
List<T> updateLocalList = new ArrayList<>();
List<T> deleteList = new ArrayList<>();
// 遍历对照数据,识别新增和更新
for (T compareRecord : compareRecords) {
K key = keyMapper.apply(compareRecord);
T local = localMap.get(key);
if (local == null) {
// 本地不存在,需要新增
insertList.add(compareRecord);
} else {
// 本地已存在,需要更新
updateLocalList.add(local);
updateCompareList.add(compareRecord);
// 标记为已处理
localMap.remove(key);
}
}
// 剩余的就是本地存在但对照组中不存在的,需要删除
for (Map.Entry<K, T> entry : localMap.entrySet()) {
deleteList.add(entry.getValue());
}
// 执行批量操作
if (CollUtil.isNotEmpty(insertList)) {
callback.doSaveBatch(insertList);
}
if (CollUtil.isNotEmpty(updateLocalList)) {
callback.doUpdateBatch(updateLocalList, updateCompareList);
}
if (CollUtil.isNotEmpty(deleteList)) {
callback.doDeleteBatch(deleteList);
}
}
/**
* 使用示例
*/
public static void main(String[] args) {
List<KitBaseUserTimeTb> localList = new ArrayList<>();
List<KitBaseUserTimeTb> onlineList = new ArrayList<>();
KitDiffSyncConstTool<KitBaseUserTimeTb> diffSyncTool = new KitDiffSyncConstTool<>(localList, onlineList);
diffSyncTool.sync(KitBaseUserTimeTb::getCreateBy, new SyncCallback<>() {
@Override
public void doSaveBatch(List<KitBaseUserTimeTb> records) {
// 全量保存
}
@Override
public void doUpdateBatch(List<KitBaseUserTimeTb> localList, List<KitBaseUserTimeTb> compareList) {
// 将 compareList 中的数据,填充到对应的 localList 数据中,更新 localList 数据
}
@Override
public void doDeleteBatch(List<KitBaseUserTimeTb> records) {
// 条件删除
}
});
}
}
@@ -0,0 +1,4 @@
/**
* 存放常用的业务工具
*/
package cn.odboy.constool;
@@ -27,7 +27,7 @@ import lombok.experimental.UtilityClass;
* @author odboy * @author odboy
*/ */
@UtilityClass @UtilityClass
public class KitDynamicTableUtil { public class KitDynamicTableBuilder {
/** /**
* 渲染排序规则 * 渲染排序规则
@@ -37,7 +37,7 @@ public class KitDynamicTableUtil {
* @param <T> 渲染对象 * @param <T> 渲染对象
* @return / * @return /
*/ */
public static <M, T> QueryWrapper<M> renderOrder(KitPageArgs<T> pageArgs) { public static <M, T> QueryWrapper<M> ofOrder(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = new QueryWrapper<>(); QueryWrapper<M> wrapper = new QueryWrapper<>();
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy(); KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) { if (orderBy != null) {
@@ -54,7 +54,7 @@ public class KitDynamicTableUtil {
* @param <T> 渲染对象 * @param <T> 渲染对象
* @return / * @return /
*/ */
public static <M, T> LambdaQueryWrapper<M> renderAll(KitPageArgs<T> pageArgs) { public static <M, T> LambdaQueryWrapper<M> ofAll(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs()); QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs());
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy(); KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) { if (orderBy != null) {
@@ -25,74 +25,110 @@ import lombok.extern.slf4j.Slf4j;
@UtilityClass @UtilityClass
public final class KitStringUtil { public final class KitStringUtil {
private static final char SEPARATOR = '_'; private static final char SEPARATOR = '_';
/** /**
* 驼峰命名法工具 * 驼峰命名法工具
* *
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
*/ * toUnderScoreCase("helloWorld") = "hello_world"
public static String toCamelCase(String s) { */
if (s == null) { public static String toCamelCase(String s) {
return null; if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
} }
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length()); /**
boolean upperCase = false; * 驼峰命名法工具
for (int i = 0; i < s.length(); i++) { *
char c = s.charAt(i); * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
if (c == SEPARATOR) { * toUnderScoreCase("helloWorld") = "hello_world"
upperCase = true; */
} else if (upperCase) { public static String toCapitalizeCamelCase(String s) {
sb.append(Character.toUpperCase(c)); if (s == null) {
upperCase = false; return null;
} else { }
sb.append(c); s = toCamelCase(s);
} return s.substring(0, 1).toUpperCase() + s.substring(1);
} }
return sb.toString();
}
/** /**
* 驼峰命名法工具 * 驼峰命名法工具
* *
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
*/ * toUnderScoreCase("helloWorld") = "hello_world"
public static String toCapitalizeCamelCase(String s) { */
if (s == null) { public static String toUnderScoreCase(String s) {
return null; if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
} }
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1); /**
} * 格式化字节数
*
* @param storage 字节数
* @return /
*/
private String renderBytes(Long storage) {
if (storage == null || storage <= 0) {
return "0 Byte";
}
/** double value = storage.doubleValue();
* 驼峰命名法工具 final long KB = 1024L;
* final long MB = KB * 1024L;
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" final long GB = MB * 1024L;
*/ final long TB = GB * 1024L;
public static String toUnderScoreCase(String s) { final long PB = TB * 1024L;
if (s == null) {
return null; if (value >= PB) {
} return String.format("%.2f PB", value / PB);
StringBuilder sb = new StringBuilder(); } else if (value >= TB) {
boolean upperCase = false; return String.format("%.2f TB", value / TB);
for (int i = 0; i < s.length(); i++) { } else if (value >= GB) {
char c = s.charAt(i); return String.format("%.2f GB", value / GB);
boolean nextUpperCase = true; } else if (value >= MB) {
if (i < (s.length() - 1)) { return String.format("%.2f MB", value / MB);
nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); } else if (value >= KB) {
} return String.format("%.2f KB", value / KB);
if ((i > 0) && Character.isUpperCase(c)) { } else {
if (!upperCase || !nextUpperCase) { return String.format("%d Byte", storage);
sb.append(SEPARATOR);
} }
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
} }
return sb.toString();
}
} }
@@ -18,7 +18,7 @@ package cn.odboy.system.features;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitPageArgs; import cn.odboy.base.KitPageArgs;
import cn.odboy.feature.KitDynamicTableResponse; import cn.odboy.feature.KitDynamicTableResponse;
import cn.odboy.feature.KitDynamicTableUtil; import cn.odboy.feature.KitDynamicTableBuilder;
import cn.odboy.system.dal.dataobject.SystemMenuTb; import cn.odboy.system.dal.dataobject.SystemMenuTb;
import cn.odboy.system.dal.model.request.SystemQueryMenuArgs; import cn.odboy.system.dal.model.request.SystemQueryMenuArgs;
import cn.odboy.system.dal.mysql.SystemMenuMapper; import cn.odboy.system.dal.mysql.SystemMenuMapper;
@@ -75,7 +75,7 @@ public class CuteDynamicTableDemoApi {
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu2(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) { public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu2(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) {
// 渲染排序(非必须) // 渲染排序(非必须)
QueryWrapper<SystemMenuTb> wrapper = KitDynamicTableUtil.renderOrder(pageArgs); QueryWrapper<SystemMenuTb> wrapper = KitDynamicTableBuilder.ofOrder(pageArgs);
LambdaQueryWrapper<SystemMenuTb> lambda = wrapper.lambda(); LambdaQueryWrapper<SystemMenuTb> lambda = wrapper.lambda();
// 查询条件 // 查询条件
SystemQueryMenuArgs args = pageArgs.getArgs(); SystemQueryMenuArgs args = pageArgs.getArgs();
@@ -91,7 +91,7 @@ public class CuteDynamicTableDemoApi {
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu3(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) { public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu3(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) {
// 渲染查询条件 // 渲染查询条件
LambdaQueryWrapper<SystemMenuTb> wrapper = KitDynamicTableUtil.renderAll(pageArgs); LambdaQueryWrapper<SystemMenuTb> wrapper = KitDynamicTableBuilder.ofAll(pageArgs);
// 响应结果 // 响应结果
return ResponseEntity.ok(new KitDynamicTableResponse<>(SystemMenuDynamicTableModel.class, pageArgs, "id", systemMenuMapper, wrapper)); return ResponseEntity.ok(new KitDynamicTableResponse<>(SystemMenuDynamicTableModel.class, pageArgs, "id", systemMenuMapper, wrapper));
} }
@@ -0,0 +1,168 @@
package cn.odboy.constool;
import cn.hutool.core.collection.CollUtil;
import cn.odboy.base.KitBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 数据对比同步工具
* <p>
* 适用场景:对比两个相同类型的对象集合,以对照组(compareRecords)为基准:
* <p>
* - 本地不存在的数据 → 新增
* <p>
* - 本地存在的数据 → 更新
* <p>
* - 对照组不存在的数据 → 删除
*
* @param <T> 数据类型
* @author odboy
*/
public class KitDiffSyncConstTool<T> {
/**
* 数据同步回调接口
*
* @param <T> 数据类型
*/
public interface SyncCallback<T> {
/**
* 全量存储对照组数据
*
* @param records 需要保存的数据列表
*/
void doSaveBatch(List<T> records);
/**
* 批量更新数据
*
* @param localList 本地已存在的数据列表
* @param compareList 对照组对应的数据列表
*/
void doUpdateBatch(List<T> localList, List<T> compareList);
/**
* 批量删除数据
*
* @param records 需要删除的数据列表
*/
void doDeleteBatch(List<T> records);
}
/**
* 本地数据源
*/
private final List<T> localRecords;
/**
* 对照数据源(以对照数据源为准)
*/
private final List<T> compareRecords;
/**
* 构造函数
*
* @param localRecords 本地数据源
* @param compareRecords 对照数据源(基准数据)
*/
public KitDiffSyncConstTool(List<T> localRecords, List<T> compareRecords) {
this.localRecords = localRecords;
this.compareRecords = compareRecords;
}
/**
* 执行数据对比同步
*
* @param keyMapper 唯一键映射函数,用于比较数据唯一性(如:Entity::getId)
* @param callback 同步回调接口,处理具体的增删改操作
* @param <K> 唯一键类型
*/
public <K> void sync(Function<? super T, ? extends K> keyMapper, SyncCallback<T> callback) {
if (keyMapper == null) {
throw new BadRequestException("参数 keyMapper 必填");
}
if (callback == null) {
throw new BadRequestException("参数 callback 必填");
}
// 如果本地数据为空,直接全量保存对照数据
if (this.localRecords.isEmpty()) {
callback.doSaveBatch(this.compareRecords);
return;
}
// 构建本地数据的键值映射
Map<K, T> localMap = localRecords.stream().collect(Collectors.toMap(keyMapper, i -> i));
List<T> insertList = new ArrayList<>();
List<T> updateCompareList = new ArrayList<>();
List<T> updateLocalList = new ArrayList<>();
List<T> deleteList = new ArrayList<>();
// 遍历对照数据,识别新增和更新
for (T compareRecord : compareRecords) {
K key = keyMapper.apply(compareRecord);
T local = localMap.get(key);
if (local == null) {
// 本地不存在,需要新增
insertList.add(compareRecord);
} else {
// 本地已存在,需要更新
updateLocalList.add(local);
updateCompareList.add(compareRecord);
// 标记为已处理
localMap.remove(key);
}
}
// 剩余的就是本地存在但对照组中不存在的,需要删除
for (Map.Entry<K, T> entry : localMap.entrySet()) {
deleteList.add(entry.getValue());
}
// 执行批量操作
if (CollUtil.isNotEmpty(insertList)) {
callback.doSaveBatch(insertList);
}
if (CollUtil.isNotEmpty(updateLocalList)) {
callback.doUpdateBatch(updateLocalList, updateCompareList);
}
if (CollUtil.isNotEmpty(deleteList)) {
callback.doDeleteBatch(deleteList);
}
}
/**
* 使用示例
*/
public static void main(String[] args) {
List<KitBaseUserTimeTb> localList = new ArrayList<>();
List<KitBaseUserTimeTb> onlineList = new ArrayList<>();
KitDiffSyncConstTool<KitBaseUserTimeTb> diffSyncTool = new KitDiffSyncConstTool<>(localList, onlineList);
diffSyncTool.sync(KitBaseUserTimeTb::getCreateBy, new SyncCallback<>() {
@Override
public void doSaveBatch(List<KitBaseUserTimeTb> records) {
// 全量保存
}
@Override
public void doUpdateBatch(List<KitBaseUserTimeTb> localList, List<KitBaseUserTimeTb> compareList) {
// 将 compareList 中的数据,填充到对应的 localList 数据中,更新 localList 数据
}
@Override
public void doDeleteBatch(List<KitBaseUserTimeTb> records) {
// 条件删除
}
});
}
}
@@ -0,0 +1,4 @@
/**
* 存放常用的业务工具
*/
package cn.odboy.constool;
@@ -27,7 +27,7 @@ import lombok.experimental.UtilityClass;
* @author odboy * @author odboy
*/ */
@UtilityClass @UtilityClass
public class KitDynamicTableUtil { public class KitDynamicTableBuilder {
/** /**
* 渲染排序规则 * 渲染排序规则
@@ -37,7 +37,7 @@ public class KitDynamicTableUtil {
* @param <T> 渲染对象 * @param <T> 渲染对象
* @return / * @return /
*/ */
public static <M, T> QueryWrapper<M> renderOrder(KitPageArgs<T> pageArgs) { public static <M, T> QueryWrapper<M> ofOrder(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = new QueryWrapper<>(); QueryWrapper<M> wrapper = new QueryWrapper<>();
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy(); KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) { if (orderBy != null) {
@@ -54,7 +54,7 @@ public class KitDynamicTableUtil {
* @param <T> 渲染对象 * @param <T> 渲染对象
* @return / * @return /
*/ */
public static <M, T> LambdaQueryWrapper<M> renderAll(KitPageArgs<T> pageArgs) { public static <M, T> LambdaQueryWrapper<M> ofAll(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs()); QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs());
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy(); KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) { if (orderBy != null) {
@@ -25,74 +25,110 @@ import lombok.extern.slf4j.Slf4j;
@UtilityClass @UtilityClass
public final class KitStringUtil { public final class KitStringUtil {
private static final char SEPARATOR = '_'; private static final char SEPARATOR = '_';
/** /**
* 驼峰命名法工具 * 驼峰命名法工具
* *
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
*/ * toUnderScoreCase("helloWorld") = "hello_world"
public static String toCamelCase(String s) { */
if (s == null) { public static String toCamelCase(String s) {
return null; if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
} }
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length()); /**
boolean upperCase = false; * 驼峰命名法工具
for (int i = 0; i < s.length(); i++) { *
char c = s.charAt(i); * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
if (c == SEPARATOR) { * toUnderScoreCase("helloWorld") = "hello_world"
upperCase = true; */
} else if (upperCase) { public static String toCapitalizeCamelCase(String s) {
sb.append(Character.toUpperCase(c)); if (s == null) {
upperCase = false; return null;
} else { }
sb.append(c); s = toCamelCase(s);
} return s.substring(0, 1).toUpperCase() + s.substring(1);
} }
return sb.toString();
}
/** /**
* 驼峰命名法工具 * 驼峰命名法工具
* *
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
*/ * toUnderScoreCase("helloWorld") = "hello_world"
public static String toCapitalizeCamelCase(String s) { */
if (s == null) { public static String toUnderScoreCase(String s) {
return null; if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
} }
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1); /**
} * 格式化字节数
*
* @param storage 字节数
* @return /
*/
private String renderBytes(Long storage) {
if (storage == null || storage <= 0) {
return "0 Byte";
}
/** double value = storage.doubleValue();
* 驼峰命名法工具 final long KB = 1024L;
* final long MB = KB * 1024L;
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" final long GB = MB * 1024L;
*/ final long TB = GB * 1024L;
public static String toUnderScoreCase(String s) { final long PB = TB * 1024L;
if (s == null) {
return null; if (value >= PB) {
} return String.format("%.2f PB", value / PB);
StringBuilder sb = new StringBuilder(); } else if (value >= TB) {
boolean upperCase = false; return String.format("%.2f TB", value / TB);
for (int i = 0; i < s.length(); i++) { } else if (value >= GB) {
char c = s.charAt(i); return String.format("%.2f GB", value / GB);
boolean nextUpperCase = true; } else if (value >= MB) {
if (i < (s.length() - 1)) { return String.format("%.2f MB", value / MB);
nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); } else if (value >= KB) {
} return String.format("%.2f KB", value / KB);
if ((i > 0) && Character.isUpperCase(c)) { } else {
if (!upperCase || !nextUpperCase) { return String.format("%d Byte", storage);
sb.append(SEPARATOR);
} }
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
} }
return sb.toString();
}
} }
@@ -18,7 +18,7 @@ package cn.odboy.system.features;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitPageArgs; import cn.odboy.base.KitPageArgs;
import cn.odboy.feature.KitDynamicTableResponse; import cn.odboy.feature.KitDynamicTableResponse;
import cn.odboy.feature.KitDynamicTableUtil; import cn.odboy.feature.KitDynamicTableBuilder;
import cn.odboy.system.dal.dataobject.SystemMenuTb; import cn.odboy.system.dal.dataobject.SystemMenuTb;
import cn.odboy.system.dal.model.request.SystemQueryMenuArgs; import cn.odboy.system.dal.model.request.SystemQueryMenuArgs;
import cn.odboy.system.dal.mysql.SystemMenuMapper; import cn.odboy.system.dal.mysql.SystemMenuMapper;
@@ -75,7 +75,7 @@ public class CuteDynamicTableDemoApi {
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu2(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) { public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu2(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) {
// 渲染排序(非必须) // 渲染排序(非必须)
QueryWrapper<SystemMenuTb> wrapper = KitDynamicTableUtil.renderOrder(pageArgs); QueryWrapper<SystemMenuTb> wrapper = KitDynamicTableBuilder.ofOrder(pageArgs);
LambdaQueryWrapper<SystemMenuTb> lambda = wrapper.lambda(); LambdaQueryWrapper<SystemMenuTb> lambda = wrapper.lambda();
// 查询条件 // 查询条件
SystemQueryMenuArgs args = pageArgs.getArgs(); SystemQueryMenuArgs args = pageArgs.getArgs();
@@ -91,7 +91,7 @@ public class CuteDynamicTableDemoApi {
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu3(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) { public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu3(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) {
// 渲染查询条件 // 渲染查询条件
LambdaQueryWrapper<SystemMenuTb> wrapper = KitDynamicTableUtil.renderAll(pageArgs); LambdaQueryWrapper<SystemMenuTb> wrapper = KitDynamicTableBuilder.ofAll(pageArgs);
// 响应结果 // 响应结果
return ResponseEntity.ok(new KitDynamicTableResponse<>(SystemMenuDynamicTableModel.class, pageArgs, "id", systemMenuMapper, wrapper)); return ResponseEntity.ok(new KitDynamicTableResponse<>(SystemMenuDynamicTableModel.class, pageArgs, "id", systemMenuMapper, wrapper));
} }
@@ -0,0 +1,168 @@
package cn.odboy.constool;
import cn.hutool.core.collection.CollUtil;
import cn.odboy.base.KitBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 数据对比同步工具
* <p>
* 适用场景:对比两个相同类型的对象集合,以对照组(compareRecords)为基准:
* <p>
* - 本地不存在的数据 → 新增
* <p>
* - 本地存在的数据 → 更新
* <p>
* - 对照组不存在的数据 → 删除
*
* @param <T> 数据类型
* @author odboy
*/
public class KitDiffSyncConstTool<T> {
/**
* 数据同步回调接口
*
* @param <T> 数据类型
*/
public interface SyncCallback<T> {
/**
* 全量存储对照组数据
*
* @param records 需要保存的数据列表
*/
void doSaveBatch(List<T> records);
/**
* 批量更新数据
*
* @param localList 本地已存在的数据列表
* @param compareList 对照组对应的数据列表
*/
void doUpdateBatch(List<T> localList, List<T> compareList);
/**
* 批量删除数据
*
* @param records 需要删除的数据列表
*/
void doDeleteBatch(List<T> records);
}
/**
* 本地数据源
*/
private final List<T> localRecords;
/**
* 对照数据源(以对照数据源为准)
*/
private final List<T> compareRecords;
/**
* 构造函数
*
* @param localRecords 本地数据源
* @param compareRecords 对照数据源(基准数据)
*/
public KitDiffSyncConstTool(List<T> localRecords, List<T> compareRecords) {
this.localRecords = localRecords;
this.compareRecords = compareRecords;
}
/**
* 执行数据对比同步
*
* @param keyMapper 唯一键映射函数,用于比较数据唯一性(如:Entity::getId)
* @param callback 同步回调接口,处理具体的增删改操作
* @param <K> 唯一键类型
*/
public <K> void sync(Function<? super T, ? extends K> keyMapper, SyncCallback<T> callback) {
if (keyMapper == null) {
throw new BadRequestException("参数 keyMapper 必填");
}
if (callback == null) {
throw new BadRequestException("参数 callback 必填");
}
// 如果本地数据为空,直接全量保存对照数据
if (this.localRecords.isEmpty()) {
callback.doSaveBatch(this.compareRecords);
return;
}
// 构建本地数据的键值映射
Map<K, T> localMap = localRecords.stream().collect(Collectors.toMap(keyMapper, i -> i));
List<T> insertList = new ArrayList<>();
List<T> updateCompareList = new ArrayList<>();
List<T> updateLocalList = new ArrayList<>();
List<T> deleteList = new ArrayList<>();
// 遍历对照数据,识别新增和更新
for (T compareRecord : compareRecords) {
K key = keyMapper.apply(compareRecord);
T local = localMap.get(key);
if (local == null) {
// 本地不存在,需要新增
insertList.add(compareRecord);
} else {
// 本地已存在,需要更新
updateLocalList.add(local);
updateCompareList.add(compareRecord);
// 标记为已处理
localMap.remove(key);
}
}
// 剩余的就是本地存在但对照组中不存在的,需要删除
for (Map.Entry<K, T> entry : localMap.entrySet()) {
deleteList.add(entry.getValue());
}
// 执行批量操作
if (CollUtil.isNotEmpty(insertList)) {
callback.doSaveBatch(insertList);
}
if (CollUtil.isNotEmpty(updateLocalList)) {
callback.doUpdateBatch(updateLocalList, updateCompareList);
}
if (CollUtil.isNotEmpty(deleteList)) {
callback.doDeleteBatch(deleteList);
}
}
/**
* 使用示例
*/
public static void main(String[] args) {
List<KitBaseUserTimeTb> localList = new ArrayList<>();
List<KitBaseUserTimeTb> onlineList = new ArrayList<>();
KitDiffSyncConstTool<KitBaseUserTimeTb> diffSyncTool = new KitDiffSyncConstTool<>(localList, onlineList);
diffSyncTool.sync(KitBaseUserTimeTb::getCreateBy, new SyncCallback<>() {
@Override
public void doSaveBatch(List<KitBaseUserTimeTb> records) {
// 全量保存
}
@Override
public void doUpdateBatch(List<KitBaseUserTimeTb> localList, List<KitBaseUserTimeTb> compareList) {
// 将 compareList 中的数据,填充到对应的 localList 数据中,更新 localList 数据
}
@Override
public void doDeleteBatch(List<KitBaseUserTimeTb> records) {
// 条件删除
}
});
}
}
@@ -0,0 +1,4 @@
/**
* 存放常用的业务工具
*/
package cn.odboy.constool;
@@ -0,0 +1,65 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.feature;
import cn.odboy.base.KitPageArgs;
import cn.odboy.framework.mybatisplus.core.KitMpQUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.experimental.UtilityClass;
/**
* 动态表格辅助工具
*
* @author odboy
*/
@UtilityClass
public class KitDynamicTableBuilder {
/**
* 渲染排序规则
*
* @param pageArgs 分页参数
* @param <M> 数据表对象
* @param <T> 渲染对象
* @return /
*/
public static <M, T> QueryWrapper<M> ofOrder(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = new QueryWrapper<>();
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
wrapper.orderBy(true, orderBy.isAsc(), orderBy.getColumn());
}
return wrapper;
}
/**
* 渲染所有规则(需要配合 @KitMpQuery 注解使用)
*
* @param pageArgs 分页参数
* @param <M> 数据表对象
* @param <T> 渲染对象
* @return /
*/
public static <M, T> LambdaQueryWrapper<M> ofAll(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs());
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
wrapper.orderBy(true, orderBy.isAsc(), orderBy.getColumn());
}
return wrapper.lambda();
}
}
@@ -1,65 +0,0 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.feature;
import cn.odboy.base.KitPageArgs;
import cn.odboy.framework.mybatisplus.core.KitMpQUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.experimental.UtilityClass;
/**
* 动态表格辅助工具
*
* @author odboy
*/
@UtilityClass
public class KitDynamicTableUtil {
/**
* 渲染排序规则
*
* @param pageArgs 分页参数
* @param <M> 数据表对象
* @param <T> 渲染对象
* @return /
*/
public static <M, T> QueryWrapper<M> renderOrder(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = new QueryWrapper<>();
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
wrapper.orderBy(true, orderBy.isAsc(), orderBy.getColumn());
}
return wrapper;
}
/**
* 渲染所有规则(需要配合 @KitMpQuery 注解使用)
*
* @param pageArgs 分页参数
* @param <M> 数据表对象
* @param <T> 渲染对象
* @return /
*/
public static <M, T> LambdaQueryWrapper<M> renderAll(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs());
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
wrapper.orderBy(true, orderBy.isAsc(), orderBy.getColumn());
}
return wrapper.lambda();
}
}
@@ -25,74 +25,110 @@ import lombok.extern.slf4j.Slf4j;
@UtilityClass @UtilityClass
public final class KitStringUtil { public final class KitStringUtil {
private static final char SEPARATOR = '_'; private static final char SEPARATOR = '_';
/** /**
* 驼峰命名法工具 * 驼峰命名法工具
* *
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
*/ * toUnderScoreCase("helloWorld") = "hello_world"
public static String toCamelCase(String s) { */
if (s == null) { public static String toCamelCase(String s) {
return null; if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
} }
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length()); /**
boolean upperCase = false; * 驼峰命名法工具
for (int i = 0; i < s.length(); i++) { *
char c = s.charAt(i); * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
if (c == SEPARATOR) { * toUnderScoreCase("helloWorld") = "hello_world"
upperCase = true; */
} else if (upperCase) { public static String toCapitalizeCamelCase(String s) {
sb.append(Character.toUpperCase(c)); if (s == null) {
upperCase = false; return null;
} else { }
sb.append(c); s = toCamelCase(s);
} return s.substring(0, 1).toUpperCase() + s.substring(1);
} }
return sb.toString();
}
/** /**
* 驼峰命名法工具 * 驼峰命名法工具
* *
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" * @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
*/ * toUnderScoreCase("helloWorld") = "hello_world"
public static String toCapitalizeCamelCase(String s) { */
if (s == null) { public static String toUnderScoreCase(String s) {
return null; if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
} }
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1); /**
} * 格式化字节数
*
* @param storage 字节数
* @return /
*/
private String renderBytes(Long storage) {
if (storage == null || storage <= 0) {
return "0 Byte";
}
/** double value = storage.doubleValue();
* 驼峰命名法工具 final long KB = 1024L;
* final long MB = KB * 1024L;
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world" final long GB = MB * 1024L;
*/ final long TB = GB * 1024L;
public static String toUnderScoreCase(String s) { final long PB = TB * 1024L;
if (s == null) {
return null; if (value >= PB) {
} return String.format("%.2f PB", value / PB);
StringBuilder sb = new StringBuilder(); } else if (value >= TB) {
boolean upperCase = false; return String.format("%.2f TB", value / TB);
for (int i = 0; i < s.length(); i++) { } else if (value >= GB) {
char c = s.charAt(i); return String.format("%.2f GB", value / GB);
boolean nextUpperCase = true; } else if (value >= MB) {
if (i < (s.length() - 1)) { return String.format("%.2f MB", value / MB);
nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); } else if (value >= KB) {
} return String.format("%.2f KB", value / KB);
if ((i > 0) && Character.isUpperCase(c)) { } else {
if (!upperCase || !nextUpperCase) { return String.format("%d Byte", storage);
sb.append(SEPARATOR);
} }
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
} }
return sb.toString();
}
} }
@@ -18,7 +18,7 @@ package cn.odboy.system.features;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitPageArgs; import cn.odboy.base.KitPageArgs;
import cn.odboy.feature.KitDynamicTableResponse; import cn.odboy.feature.KitDynamicTableResponse;
import cn.odboy.feature.KitDynamicTableUtil; import cn.odboy.feature.KitDynamicTableBuilder;
import cn.odboy.system.dal.dataobject.SystemMenuTb; import cn.odboy.system.dal.dataobject.SystemMenuTb;
import cn.odboy.system.dal.model.request.SystemQueryMenuArgs; import cn.odboy.system.dal.model.request.SystemQueryMenuArgs;
import cn.odboy.system.dal.mysql.SystemMenuMapper; import cn.odboy.system.dal.mysql.SystemMenuMapper;
@@ -81,7 +81,7 @@ public class CuteDynamicTableDemoApi {
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu2(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) { public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu2(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) {
// 渲染排序(非必须) // 渲染排序(非必须)
QueryWrapper<SystemMenuTb> wrapper = KitDynamicTableUtil.renderOrder(pageArgs); QueryWrapper<SystemMenuTb> wrapper = KitDynamicTableBuilder.ofOrder(pageArgs);
LambdaQueryWrapper<SystemMenuTb> lambda = wrapper.lambda(); LambdaQueryWrapper<SystemMenuTb> lambda = wrapper.lambda();
// 查询条件 // 查询条件
SystemQueryMenuArgs args = pageArgs.getArgs(); SystemQueryMenuArgs args = pageArgs.getArgs();
@@ -98,7 +98,7 @@ public class CuteDynamicTableDemoApi {
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu3(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) { public ResponseEntity<KitDynamicTableResponse<SystemMenuTb, SystemMenuDynamicTableModel>> searchMenu3(@Validated @RequestBody KitPageArgs<SystemQueryMenuArgs> pageArgs) {
// 渲染查询条件 // 渲染查询条件
LambdaQueryWrapper<SystemMenuTb> wrapper = KitDynamicTableUtil.renderAll(pageArgs); LambdaQueryWrapper<SystemMenuTb> wrapper = KitDynamicTableBuilder.ofAll(pageArgs);
// 响应结果 // 响应结果
return ResponseEntity.ok(new KitDynamicTableResponse<>(SystemMenuDynamicTableModel.class, pageArgs, "id", systemMenuMapper, wrapper)); return ResponseEntity.ok(new KitDynamicTableResponse<>(SystemMenuDynamicTableModel.class, pageArgs, "id", systemMenuMapper, wrapper));
} }
@@ -0,0 +1,51 @@
package cn.odboy;
import cn.odboy.system.dal.dataobject.SystemDeptTb;
import cn.odboy.util.KitClassUtil;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Mybatis批量操作生成器
*
* @author odboy
*/
public class MybatisMapperBatchOpRender {
/**
* 持久类名
*/
private String entityClassName;
/**
* 持久类主键类型名
*/
private String entityPkTypeName;
/**
* 渲染MapperXml
*
* @param entityClass 持久类
* @param keyMapper 哪个是主键
*/
public <T, K> void render(Class<?> entityClass, Function<? super T, ? extends K> keyMapper) {
String mapperBatchInsert = "void batchInsert(@Param(\"list\") List<$EntityClassName> list);";
System.err.println(mapperBatchInsert.replace("$EntityClassName", this.entityClassName));
String mapperBatchUpdate = "void batchUpdateById(@Param(\"list\") List<$EntityClassName> list);";
System.err.println(mapperBatchUpdate.replace("$EntityClassName", this.entityClassName));
String mapperBatchDelete = "void batchDeleteByIds(@Param(\"list\") List<$EntityPkTypeName> list);";
System.err.println(mapperBatchDelete.replace("$EntityPkTypeName", this.entityPkTypeName));
List<Field> fields = new ArrayList<>();
KitClassUtil.getAllFields(entityClass, fields);
fields = fields.stream().filter(f -> !"serialVersionUID".equals(f.getName())).collect(Collectors.toList());
System.err.println(fields);
}
public static void main(String[] args) {
new MybatisMapperBatchOpRender().render(SystemDeptTb.class, SystemDeptTb::getId);
}
}