From 3f5bba9a024bccaccbf29a81b8ed65251b13a1ec Mon Sep 17 00:00:00 2001 From: George Date: Sun, 23 Aug 2026 16:27:10 +0800 Subject: [PATCH 01/19] =?UTF-8?q?20260823:H5=E9=A1=B5=E9=9D=A2=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=95=B0=E6=8D=AE=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- yack-app/apis/modules/mqttTest.js | 42 ++ yack-app/env.config.js | 4 +- yack-app/locale/en-US.json | 68 ++ yack-app/locale/zh-CN.json | 68 ++ yack-app/pages.json | 20 + yack-app/pages/tabBar/home/index.vue | 3 +- yack-app/pagesA/home/device/index.vue | 17 +- yack-app/pagesA/home/device/log/index.vue | 13 + yack-app/pagesB/home/device/debug/edit.vue | 615 ++++++++++++++++++ yack-app/pagesB/home/device/debug/index.vue | 655 ++++++++++++++++++++ yack-app/utils/common.js | 128 +++- 12 files changed, 1625 insertions(+), 11 deletions(-) create mode 100644 yack-app/apis/modules/mqttTest.js create mode 100644 yack-app/pagesB/home/device/debug/edit.vue create mode 100644 yack-app/pagesB/home/device/debug/index.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9a2dc969..b813dba5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -63,7 +63,8 @@ "Bash(done)", "Bash(file /tmp/tile_*.png)", "Bash(for REF in \"http://localhost:8080/\" \"https://dev.fastbee.cn/\" \"https://www.tianditu.gov.cn/\")", - "Read(//tmp/**)" + "Read(//tmp/**)", + "Bash(node -e ' *)" ] } } diff --git a/yack-app/apis/modules/mqttTest.js b/yack-app/apis/modules/mqttTest.js new file mode 100644 index 00000000..65a64364 --- /dev/null +++ b/yack-app/apis/modules/mqttTest.js @@ -0,0 +1,42 @@ +const http = uni.$u.http; + +// 指令编码 +export function encode(params) { + return http.get('/iot/message/encode', { + params: params + }); +} + +// 指令解码 +export function decode(params) { + return http.get('/iot/message/decode', { + params: params + }); +} + +// 平台下发指令 +export function messagePost(data) { + return http.post('/iot/message/post', data); +} + +// 查询设备指令偏好设置列表 +export function preferencesList(params) { + return http.get('/iot/preferences/list', { + params: params + }); +} + +// 新增设备指令偏好设置 +export function addPreferences(data) { + return http.post('/iot/preferences', data); +} + +// 修改设备指令偏好设置 +export function editPreferences(data) { + return http.put('/iot/preferences', data); +} + +// 删除设备指令偏好设置 +export function delPreferences(data) { + return http.delete('/iot/preferences/' + data.id); +} diff --git a/yack-app/env.config.js b/yack-app/env.config.js index c6f8a021..1cfea6fa 100644 --- a/yack-app/env.config.js +++ b/yack-app/env.config.js @@ -1,5 +1,5 @@ // H5端开发和生产环境协议 -let protocalDev = "wss://"; +let protocalDev = "ws://"; let protocalProd = "wss://"; // 条件编译,微信端和移动端使用wxs协议 @@ -19,7 +19,7 @@ const CONFIG = { development: { officialWebUrl: 'http://localhost:8080/', baseUrl: 'http://localhost:8080/', - mqttServer: protocalDev + 'localhost/mqtt', + mqttServer: protocalDev + '47.121.180.229:8083/mqtt', decoderUrl: 'http://localhost:8080/', qqmapKey, xinzhiKey, diff --git a/yack-app/locale/en-US.json b/yack-app/locale/en-US.json index a476c7a2..c75992af 100644 --- a/yack-app/locale/en-US.json +++ b/yack-app/locale/en-US.json @@ -49,6 +49,8 @@ "navBar.updateDevice": "Device Update", "navBar.eventLog": "Event Log", "navBar.orderLog": "Instruction log", + "navBar.dataDebug": "Data Debug", + "navBar.commandBuilder": "Command Builder", "navBar.player": "Player", "common.add": "add", "common.delete": "Delete", @@ -240,6 +242,72 @@ "createBtn": "Create Device", "content": "This feature requires login to use. Do you want to log in?" }, + "debug": { + "title": "Data Debug", + "send": "Send", + "reveiceType": "Reply", + "reportType": "Report", + "sendType": "Send", + "hex": "HEX", + "json": "JSON", + "ascii": "ASCII", + "inputPlaceholder": "Please enter the content to send", + "typePlaceholder": "Select format", + "analysis": "Parse", + "analysised": "Parsed", + "name": "Name", + "identifier": "Identifier", + "value": "Value", + "more": "More", + "quickCommand": "Quick Commands", + "addCommand": "Add Command", + "editCommand": "Edit Command", + "slaveAddress": "Slave Address", + "issueType": "Command Type", + "functionCode": "Function Code", + "startAddress": "Start Register Address", + "registerCount": "Register Count", + "coilCount": "Coil Count", + "writeCoilValue": "Coil Value", + "writeRegisterValue": "Register Value", + "registerValue": "Register Value", + "reportValue": "Report Value", + "coilValue": "Coil Value", + "generateCommand": "Generate", + "generatedCommand": "Generated Command", + "saveAsCommand": "Save As Command", + "copy": "Copy", + "cancel": "Cancel", + "confirm": "Confirm", + "commandName": "Command Name", + "inputCommandName": "Please enter command name", + "commandContent": "Command Content", + "commandScope": "Command Scope", + "productCommand": "Product Command (shared by same product)", + "deviceCommand": "Device Command (this device only)", + "deleteCommand": "Delete Command", + "deleteConfirm": "Are you sure to delete this command?", + "reportCommand": "Device Report", + "issueCommand": "Platform Issue", + "code01": "01 Read Coils", + "code02": "02 Read Discrete Inputs", + "code03": "03 Read Holding Registers", + "code04": "04 Read Input Registers", + "code05": "05 Write Single Coil", + "code06": "06 Write Single Register", + "code15": "15 Write Multiple Coils", + "code16": "16 Write Multiple Registers", + "sendEmpty": "Please enter the content to send", + "sendFail": "Send failed", + "analysisEmpty": "No data to parse", + "analysisFail": "Parse failed", + "generateFail": "Generate failed", + "saveSuccess": "Saved successfully", + "saveFail": "Save failed", + "deleteSuccess": "Deleted successfully", + "deleteFail": "Delete failed", + "copySuccess": "Copied successfully" + }, "about": { "about": "about", "authorQQ": "Author QQ : 164770707", diff --git a/yack-app/locale/zh-CN.json b/yack-app/locale/zh-CN.json index 9bcf2f3c..0cf9d83e 100644 --- a/yack-app/locale/zh-CN.json +++ b/yack-app/locale/zh-CN.json @@ -46,6 +46,8 @@ "navBar.updateDevice": "设备修改", "navBar.eventLog": "事件日志", "navBar.orderLog": "指令日志", + "navBar.dataDebug": "数据调试", + "navBar.commandBuilder": "指令生成", "navBar.player": "播放器", "common.fastbee": "岩安测控", "common.add": "添加", @@ -241,6 +243,72 @@ "createBtn": "创建设备", "content": "该功能需要登录才可使用,是否去登录?" }, + "debug": { + "title": "数据调试", + "send": "发送", + "reveiceType": "回复", + "reportType": "上报", + "sendType": "下发", + "hex": "HEX", + "json": "JSON", + "ascii": "ASCII", + "inputPlaceholder": "请输入要发送的内容", + "typePlaceholder": "选择格式", + "analysis": "解析", + "analysised": "已解析", + "name": "参数名", + "identifier": "标识符", + "value": "参数值", + "more": "更多", + "quickCommand": "快捷指令", + "addCommand": "新建指令", + "editCommand": "编辑指令", + "slaveAddress": "从机地址", + "issueType": "指令类型", + "functionCode": "功能码", + "startAddress": "起始寄存器地址", + "registerCount": "寄存器个数", + "coilCount": "线圈个数", + "writeCoilValue": "线圈值", + "writeRegisterValue": "寄存器值", + "registerValue": "寄存器值", + "reportValue": "上报值", + "coilValue": "线圈值", + "generateCommand": "生成指令", + "generatedCommand": "生成的指令", + "saveAsCommand": "存为指令", + "copy": "复制", + "cancel": "取消", + "confirm": "确定", + "commandName": "指令名称", + "inputCommandName": "请输入指令名称", + "commandContent": "指令内容", + "commandScope": "指令范围", + "productCommand": "产品指令(同类产品通用)", + "deviceCommand": "设备指令(仅本设备)", + "deleteCommand": "删除指令", + "deleteConfirm": "确认删除该指令吗?", + "reportCommand": "设备上报", + "issueCommand": "平台下发", + "code01": "01 读线圈", + "code02": "02 读离散输入", + "code03": "03 读保持寄存器", + "code04": "04 读输入寄存器", + "code05": "05 写单个线圈", + "code06": "06 写单个寄存器", + "code15": "15 写多个线圈", + "code16": "16 写多个寄存器", + "sendEmpty": "请输入要发送的内容", + "sendFail": "发送失败", + "analysisEmpty": "暂无可解析的数据", + "analysisFail": "解析失败", + "generateFail": "生成失败", + "saveSuccess": "保存成功", + "saveFail": "保存失败", + "deleteSuccess": "删除成功", + "deleteFail": "删除失败", + "copySuccess": "复制成功" + }, "about": { "about": "关于", "authorQQ": "作者企鹅:164770707", diff --git a/yack-app/pages.json b/yack-app/pages.json index 9cdedb1f..c4c217e0 100644 --- a/yack-app/pages.json +++ b/yack-app/pages.json @@ -533,6 +533,26 @@ "enablePullDownRefresh": true } }, + { + "path": "home/device/debug/index", + "meta": { + "title": "%navBar.dataDebug%" + }, + "style": { + "navigationBarTitleText": "%navBar.dataDebug%", + "enablePullDownRefresh": false + } + }, + { + "path": "home/device/debug/edit", + "meta": { + "title": "%navBar.commandBuilder%" + }, + "style": { + "navigationBarTitleText": "%navBar.commandBuilder%", + "enablePullDownRefresh": false + } + }, { "path": "login/register", "style": { diff --git a/yack-app/pages/tabBar/home/index.vue b/yack-app/pages/tabBar/home/index.vue index ddc65e4f..8f17acea 100644 --- a/yack-app/pages/tabBar/home/index.vue +++ b/yack-app/pages/tabBar/home/index.vue @@ -242,7 +242,8 @@ pageSize: 10, groupId: 0, deviceName: null, - showChild: true + showChild: true, + entrance: 2 }, loadStatus: 'loadmore', // 加载更多 deviceList: [], // 设备列表 diff --git a/yack-app/pagesA/home/device/index.vue b/yack-app/pagesA/home/device/index.vue index 95d4ef01..334b0a60 100644 --- a/yack-app/pagesA/home/device/index.vue +++ b/yack-app/pagesA/home/device/index.vue @@ -331,12 +331,17 @@ this.deviceForm.rssid = message.rssid; } }; - //不是modbus不转发到子页面,其他设备的页面有回调方法 - if (this.isSubDev) { - /*发送设备上报到子模块*/ - if (topic.endsWith('ws/service')) { - console.log('接收到【设备数据上报】主题:', topic) - console.log('接收到【设备数据上报】主题:', message) + /*发送设备上报到数据调试页面*/ + if (topic.endsWith('ws/service')) { + console.log('接收到【设备数据上报】主题:', topic) + console.log('接收到【设备数据上报】主题:', message) + this.$bus.$emit("updateData", { + serialNumber: topics[2], + productId: this.deviceForm.productId, + data: message, + }) + //不是modbus不转发到子页面,其他设备的页面有回调方法 + if (this.isSubDev) { this.$bus.$emit("updateDeviceStatus", { serialNumber: topics[2], productId: this.deviceForm.productId, diff --git a/yack-app/pagesA/home/device/log/index.vue b/yack-app/pagesA/home/device/log/index.vue index 859b80a5..5d0f9c63 100644 --- a/yack-app/pagesA/home/device/log/index.vue +++ b/yack-app/pagesA/home/device/log/index.vue @@ -6,6 +6,8 @@ @click="handleEventLogClick"> + @@ -58,6 +60,17 @@ uni.navigateTo({ url: `/pagesB/home/device/log/order?deviceId=${this.deviceId}&serialNumber=${this.serialNumber}` }); + }, + handleDebugClick () { + const device = { + deviceId: this.deviceInfo.deviceId, + serialNumber: this.deviceInfo.serialNumber, + productId: this.deviceInfo.productId, + protocolCode: this.deviceInfo.protocolCode, + }; + uni.navigateTo({ + url: `/pagesB/home/device/debug/index?device=${encodeURIComponent(JSON.stringify(device))}` + }); } } }; diff --git a/yack-app/pagesB/home/device/debug/edit.vue b/yack-app/pagesB/home/device/debug/edit.vue new file mode 100644 index 00000000..0861df5f --- /dev/null +++ b/yack-app/pagesB/home/device/debug/edit.vue @@ -0,0 +1,615 @@ + + + + + + + \ No newline at end of file diff --git a/yack-app/pagesB/home/device/debug/index.vue b/yack-app/pagesB/home/device/debug/index.vue new file mode 100644 index 00000000..321ab823 --- /dev/null +++ b/yack-app/pagesB/home/device/debug/index.vue @@ -0,0 +1,655 @@ + + + + + + + \ No newline at end of file diff --git a/yack-app/utils/common.js b/yack-app/utils/common.js index f6a27579..88f53543 100644 --- a/yack-app/utils/common.js +++ b/yack-app/utils/common.js @@ -26,7 +26,7 @@ export function tansParams (params) { /** * 参数处理,避免小程序跳转10层后无法跳转 - * @param url + * @param url */ export function navigateBackTo (url) { const backUrl = url.slice(1); @@ -35,4 +35,130 @@ export function navigateBackTo (url) { const index = pages.findIndex(item => item.route === backUrl); const backnum = num - index - 1; uni.navigateBack({ delta: backnum }); +} + +/** + * 深复制 + * @param {any} obj 需要复制的对象 + */ +export function deepClone (obj) { + if (typeof obj == 'object') { + const objClone = Array.isArray(obj) ? [] : {} + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (obj[key] && typeof obj[key] == 'object') { + objClone[key] = deepClone(obj[key]) + } else { + objClone[key] = obj[key] + } + } + } + return objClone + } + return obj +} + +/** + * @desc 时间戳转日期毫秒 + * @param {Date} format 时间格式 Y:年,M:月,D:天 h:小时,m:分钟,s:秒 默认:'Y/M/D h:m' + */ +export function formatDate (arg, format) { + let now + if (!arg) { + return null + } else { + now = new Date(arg) + } + format = format || 'Y.M.D h:m' + + const year = now.getFullYear() + let month = now.getMonth() + 1 + month = month >= 10 ? month : '0' + month + let date = now.getDate() + date = date >= 10 ? date : '0' + date + let hour = now.getHours() + hour = hour >= 10 ? hour : '0' + hour + let minute = now.getMinutes() + minute = minute >= 10 ? minute : '0' + minute + let second = now.getSeconds() + second = second >= 10 ? second : '0' + second + return format + .replace('Y', year) + .replace('M', month) + .replace('D', date) + .replace('h', hour) + .replace('m', minute) + .replace('s', second) +} + +/** + * 复制文本(uni-app 剪贴板) + * @param {string} copytext + */ +export function copyText (copytext) { + uni.setClipboardData({ + data: copytext, + success: () => {}, + }) + return { type: 'success', message: '复制成功' } +} + +/** + * hex转10进制 + * @param hexStr + */ +export function hex2int (hexStr) { + let twoStr = parseInt(hexStr, 16).toString(2) // 将十六转十进制,再转2进制 + let bitNum = hexStr.length * 4 // 1个字节 = 8bit ,0xff 一个 "f"就是4位 + if (twoStr.length < bitNum) { + while (twoStr.length < bitNum) { + twoStr = "0" + twoStr + } + } + if (twoStr.substring(0, 1) == "0") { + // 正数 + const two = parseInt(twoStr, 2) // 二进制转十进制 + return two + } else { + // 负数 + let twoStr_unsign = "" + let two = parseInt(twoStr, 2) - 1 // 补码:(负数)反码+1,符号位不变;相对十进制来说也是 +1,但这里是负数,+1就是绝对值数据-1 + twoStr = two.toString(2) + twoStr_unsign = twoStr.substring(1, bitNum) // 舍弃首位(符号位) + // 去除首字符,将0转为1,将1转为0 反码 + twoStr_unsign = twoStr_unsign.replace(/0/g, "z") + twoStr_unsign = twoStr_unsign.replace(/1/g, "0") + twoStr_unsign = twoStr_unsign.replace(/z/g, "1") + two = -parseInt(twoStr_unsign, 2) + return two + } +} + +/** + * 10进制转16进制 + * @param hexStr + */ +export function int2hex (hexStr) { + let hexInt = typeof hexStr == 'string' ? parseInt(hexStr, 10) : hexStr + let hex = '' + const LENGTH_04 = 4 + const LENGTH_15 = 15 + if (hexInt >= 0) { + hex = hexInt.toString(16).toLocaleLowerCase() + } else { + let twoStr = (-hexInt - 1).toString(2) + let sb = '000000000000000' + twoStr = sb.slice(0, LENGTH_15 - twoStr.length) + twoStr + // 将0转为1,将1转为0 反码 + twoStr = twoStr.replace(/0/g, "z") + twoStr = twoStr.replace(/1/g, "0") + twoStr = twoStr.replace(/z/g, "1") + hex = parseInt(('1' + twoStr), 2).toString(16).toLocaleLowerCase() + } + // 长度不等于4 补全 + if (hex.length != LENGTH_04) { + let sb = "0000" + hex = sb.slice(0, LENGTH_04 - hex.length) + hex + } + return hex.toUpperCase() } \ No newline at end of file From 7539dd33beeae906c43128ddcc9e481a22b0c783 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 23 Aug 2026 16:51:42 +0800 Subject: [PATCH 02/19] =?UTF-8?q?20260823:H5=E4=BF=AE=E5=A4=8D=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E7=BD=91=E5=85=B3=E8=AE=BE=E5=A4=87=E7=89=A9=E7=90=86?= =?UTF-8?q?=E9=87=8Fbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yack-app/apis/modules/deviceLog.js | 11 +- yack-app/locale/en-US.json | 8 + yack-app/locale/zh-CN.json | 8 + .../pagesA/home/device/statistic/history.vue | 251 ++++++++++++------ 4 files changed, 197 insertions(+), 81 deletions(-) diff --git a/yack-app/apis/modules/deviceLog.js b/yack-app/apis/modules/deviceLog.js index a17f4987..bf46db17 100644 --- a/yack-app/apis/modules/deviceLog.js +++ b/yack-app/apis/modules/deviceLog.js @@ -32,7 +32,7 @@ export function getDeviceHistory (params) { return http.get('/iot/deviceLog/history', { params: params }); } -//查询历史监测数据 +//查询历史监测数据(直连设备) export function getHistoryList (data) { return http.request({ url: '/data/center/deviceHistory', @@ -41,6 +41,15 @@ export function getHistoryList (data) { }) } +// 查询网关子设备对比历史数据 +export function getSubDeviceComparativeHistory (data) { + return http.request({ + url: '/data/center/subDeviceComparativeHistory', + method: 'post', + data: data, + }) +} + // 查询设备日志详细 export function getDeviceLog (logId) { return http.request({ diff --git a/yack-app/locale/en-US.json b/yack-app/locale/en-US.json index c75992af..a287bfd8 100644 --- a/yack-app/locale/en-US.json +++ b/yack-app/locale/en-US.json @@ -812,7 +812,15 @@ }, "deviceHistory": { "lastTwoHours": "last 2 hours", + "lastFourHours": "last 4 hours", + "lastEightHours": "last 8 hours", + "lastTwelveHours": "last 12 hours", "lastOneDay": "last 1 day", + "lastTwoDays": "last 2 days", + "lastThreeDays": "last 3 days", + "lastFiveDays": "last 5 days", + "lastSevenDays": "last 7 days", + "lastFifteenDays": "last 15 days", "lastThirtyDay": "last 30 day", "custom": "custom", "inputDate": "starting time", diff --git a/yack-app/locale/zh-CN.json b/yack-app/locale/zh-CN.json index 0cf9d83e..e0ff109a 100644 --- a/yack-app/locale/zh-CN.json +++ b/yack-app/locale/zh-CN.json @@ -814,7 +814,15 @@ }, "deviceHistory": { "lastTwoHours": "最近2小时", + "lastFourHours": "最近4小时", + "lastEightHours": "最近8小时", + "lastTwelveHours": "最近12小时", "lastOneDay": "最近1天", + "lastTwoDays": "最近2天", + "lastThreeDays": "最近3天", + "lastFiveDays": "最近5天", + "lastSevenDays": "最近7天", + "lastFifteenDays": "最近15天", "lastThirtyDay": "最近30天", "custom": "自定义", "inputDate": "起始时间", diff --git a/yack-app/pagesA/home/device/statistic/history.vue b/yack-app/pagesA/home/device/statistic/history.vue index 4ae8924b..3fd3c20a 100644 --- a/yack-app/pagesA/home/device/statistic/history.vue +++ b/yack-app/pagesA/home/device/statistic/history.vue @@ -81,7 +81,7 @@ import uniPagination from '@/pagesA/components/uni-pagination/index.vue'; import { getSubGatewayList } from '@/apis/modules/gateway.js'; import { listThingsModel } from '@/apis/modules/device.js'; - import { getHistoryList } from '@/apis/modules/deviceLog.js'; + import { getHistoryList, getSubDeviceComparativeHistory } from '@/apis/modules/deviceLog.js'; export default { name: 'deviceHistory', @@ -118,27 +118,59 @@ return { deviceInfo: {}, // 设备信息 deviceId: null, - // 时间快捷数据列表 + // 时间快捷数据列表(value 为小时数,-1 表示自定义) timeShortcuts: [{ - value: 0, + value: 2, text: this.$tt('deviceHistory.lastTwoHours') }, { - value: 1, + value: 4, + text: this.$tt('deviceHistory.lastFourHours') + }, + { + value: 8, + text: this.$tt('deviceHistory.lastEightHours') + }, + { + value: 12, + text: this.$tt('deviceHistory.lastTwelveHours') + }, + { + value: 24, text: this.$tt('deviceHistory.lastOneDay') }, { - value: 2, + value: 48, + text: this.$tt('deviceHistory.lastTwoDays') + }, + { + value: 72, + text: this.$tt('deviceHistory.lastThreeDays') + }, + { + value: 120, + text: this.$tt('deviceHistory.lastFiveDays') + }, + { + value: 168, + text: this.$tt('deviceHistory.lastSevenDays') + }, + { + value: 360, + text: this.$tt('deviceHistory.lastFifteenDays') + }, + { + value: 720, text: this.$tt('deviceHistory.lastThirtyDay') }, { - value: 3, + value: -1, text: this.$tt('deviceHistory.custom') }, ], // 查询参数 queryParams: { - timeShortcut: 1, + timeShortcut: 24, strTime: `${beginTime}-${endTime}`, beginTime: beginTime, endTime: endTime, @@ -201,20 +233,13 @@ }, }, methods: { - // 快捷时间选择 + // 快捷时间选择(val 为小时数,-1 表示自定义) handleTimeShortcutChange (val) { - if (val === 3) { + if (val === -1) { return; } const endTime = new Date().getTime(); - let beginTime = null; - if (val === 0) { - beginTime = endTime - 3600 * 1000 * 2; - } else if (val === 1) { - beginTime = endTime - 3600 * 1000 * 24; - } else if (val === 2) { - beginTime = endTime - 3600 * 1000 * 24 * 30; - }; + const beginTime = endTime - 3600 * 1000 * val; this.queryParams.beginTime = beginTime; this.queryParams.endTime = endTime; this.queryParams.strTime = beginTime + '-' + endTime; @@ -263,7 +288,7 @@ this.queryParams.strTime = beginTime + '-' + e.value; this.getHistory(); this.isDateTime = false; - this.queryParams.timeShortcut = 3; + this.queryParams.timeShortcut = -1; } }, // 取消日期选择 @@ -322,9 +347,9 @@ } }); }, - // 获取物模型 + // 获取物模型(网关设备按identifier去重) getIdentifierList () { - const { deviceId } = this.deviceInfo; + const { deviceId, deviceType } = this.deviceInfo; const params = { deviceId: deviceId, pageNum: 1, @@ -341,9 +366,23 @@ type: item.type, } } - }); - this.identifierList = this.identifierList.filter(item => item !== undefined); - this.queryParams.identifiers = [this.identifierList[0].value]; + }).filter(item => item !== undefined); + + // 网关设备:按identifier去重(多个子设备可能有相同identifier) + if (deviceType === 2) { + const seen = new Set(); + this.identifierList = this.identifierList.filter(item => { + if (seen.has(item.value)) { + return false; + } + seen.add(item.value); + return true; + }); + } + + if (this.identifierList.length > 0) { + this.queryParams.identifiers = [this.identifierList[0].value]; + } } }); }, @@ -354,68 +393,120 @@ }, // 获取统计数据 getHistory () { - const { identifiers, beginTime, endTime, slaveId } = this.queryParams; - const { deviceId, serialNumber } = this.deviceInfo; + const { identifiers, beginTime, endTime } = this.queryParams; + const { deviceId, serialNumber, deviceType } = this.deviceInfo; if (identifiers.length !== 0) { - const idenList = identifiers.map((item) => { - const iden = this.identifierList.find((chil) => chil.value === item); + this.historys = []; + if (deviceType === 2) { + // 网关设备:调用多设备对比接口,数据按子设备SN存储 + const params = { + gwDeviceId: deviceId, + identifierList: identifiers, + type: 1, + beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'), + endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'), + }; + getSubDeviceComparativeHistory(params).then(res => { + if (res.code === 200) { + this.historys = res.data || []; + this.chartData = this.getChartDatas(); + this.getTableDatas(); + } + }) + } else { + // 直连设备 + const idenList = identifiers.map((item) => { + const iden = this.identifierList.find((chil) => chil.value === item); + return { + identifier: iden.value, + type: iden.type + }; + }); + const params = { + identifierList: idenList, + beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'), + endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'), + deviceId: deviceId, + serialNumber: serialNumber, + }; + getHistoryList(params).then(res => { + if (res.code === 200) { + this.historys = res.data; + this.chartData = this.getChartDatas(); + this.getTableDatas(); + } + }) + } + } + }, + // 获取折线图数据(适配两种接口响应格式) + getChartDatas () { + const { deviceType } = this.deviceInfo; + if (deviceType === 2) { + // subDeviceComparativeHistory 格式: [{ "time": "2026-08-23 10:00:00", "子设备A": "25.5", "子设备B": "26.0" }] + const categories = this.historys.map((item) => item.time); + // 获取除 time 外的所有列名作为系列 + const keys = Object.keys(this.historys[0] || {}).filter(k => k !== 'time'); + const series = keys.map((key) => { return { - identifier: iden.value, - type: iden.type + name: key, + data: this.historys.map((d) => Number(d[key]) || 0), }; }); - const params = { - identifierList: idenList, - beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'), - endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'), - deviceId: deviceId, - // slaveId: slaveId, - serialNumber: serialNumber, - }; - this.historys = []; - getHistoryList(params).then(res => { - if (res.code === 200) { - this.historys = res.data; - this.chartData = this.getChartDatas(); - this.getTableDatas(); - } - }) - } - }, - // 获取折线图数据 - getChartDatas () { - const categories = this.historys.map((item) => Object.keys(item)[0]); - const series = this.queryParams.identifiers.map((item) => { - return { - name: this.identifierList.find((chil) => chil.value === item).text, - data: this.historys.map((d) => { - const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item); - return Number(Object.values(ide)[0]); - }), - }; - }); - return { - categories, - series - } - }, - // 获取表格数据 - getTableDatas () { - this.tableHeaders = this.queryParams.identifiers.map((item) => ({ - name: this.identifierList.find((chil) => chil.value === item).text, - value: item, - })); - this.tableComTemp = this.historys.map((item) => { - const time = Object.keys(item)[0]; - let obj = {}; - Object.values(item)[0].forEach((chil) => { - obj[Object.keys(chil)[0]] = Object.values(chil)[0]; + return { categories, series }; + } else { + // deviceHistory 格式: [{ "2026-08-23 10:00:00": [{ "temperature": "25.5" }] }] + const categories = this.historys.map((item) => Object.keys(item)[0]); + const series = this.queryParams.identifiers.map((item) => { + return { + name: this.identifierList.find((chil) => chil.value === item).text, + data: this.historys.map((d) => { + const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item); + return Number(Object.values(ide)[0]); + }), + }; }); - return { - time, - ...obj - }; - }); + return { categories, series }; + } + }, + // 获取表格数据(适配两种接口响应格式) + getTableDatas () { + const { deviceType } = this.deviceInfo; + if (deviceType === 2) { + // subDeviceComparativeHistory 格式 + const keys = Object.keys(this.historys[0] || {}).filter(k => k !== 'time'); + this.tableHeaders = keys.map((key) => ({ + name: key, + value: key, + })); + this.tableComTemp = this.historys.map((item) => { + let obj = {}; + keys.forEach((key) => { + obj[key] = item[key]; + }); + return { + time: item.time, + ...obj + }; + }); + } else { + // deviceHistory 格式 + this.tableHeaders = this.queryParams.identifiers.map((item) => ({ + name: this.identifierList.find((chil) => chil.value === item).text, + value: item, + })); + this.tableComTemp = this.historys.map((item) => { + const time = Object.keys(item)[0]; + let obj = {}; + Object.values(item)[0].forEach((chil) => { + obj[Object.keys(chil)[0]] = Object.values(chil)[0]; + }); + return { + time, + ...obj + }; + }); + } this.devTotal = this.historys.length; }, // 获取设备列表数据 From 3352e9ab822ccbdb37789eb4e8c790f2e28bf2ce Mon Sep 17 00:00:00 2001 From: George Date: Sun, 23 Aug 2026 18:09:21 +0800 Subject: [PATCH 03/19] =?UTF-8?q?20260823:H5=E4=BF=AE=E5=A4=8D=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E7=BD=91=E5=85=B3=E8=AE=BE=E5=A4=87=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E7=89=A9=E7=90=86=E9=87=8Fbug&=E9=A6=96=E9=A1=B5=E4=B8=80?= =?UTF-8?q?=E6=8A=8A=E6=9F=A5=E8=AF=A2=E6=89=80=E6=9C=89=E7=9A=84=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yack-app/apis/modules/deviceLog.js | 9 ++ yack-app/pages/tabBar/home/index.vue | 3 +- .../pagesA/home/device/statistic/history.vue | 143 +++++++++++++++--- 3 files changed, 131 insertions(+), 24 deletions(-) diff --git a/yack-app/apis/modules/deviceLog.js b/yack-app/apis/modules/deviceLog.js index bf46db17..ccdcec6a 100644 --- a/yack-app/apis/modules/deviceLog.js +++ b/yack-app/apis/modules/deviceLog.js @@ -50,6 +50,15 @@ export function getSubDeviceComparativeHistory (data) { }) } +// 查询网关计算物模型历史数据(查询时按公式计算) +export function getDeviceHistoryCalculated (data) { + return http.request({ + url: '/data/center/deviceHistoryCalculated', + method: 'post', + data: data, + }) +} + // 查询设备日志详细 export function getDeviceLog (logId) { return http.request({ diff --git a/yack-app/pages/tabBar/home/index.vue b/yack-app/pages/tabBar/home/index.vue index 8f17acea..90122b0c 100644 --- a/yack-app/pages/tabBar/home/index.vue +++ b/yack-app/pages/tabBar/home/index.vue @@ -365,7 +365,8 @@ // 获取分组列表 getGroups () { getGroupList({ - userId: this.profile.userId + userId: this.profile.userId, + pageSize: 99 // 一次性获取全部 }).then(res => { if (res.code === 200) { this.groupList = [{ diff --git a/yack-app/pagesA/home/device/statistic/history.vue b/yack-app/pagesA/home/device/statistic/history.vue index 3fd3c20a..53f387df 100644 --- a/yack-app/pagesA/home/device/statistic/history.vue +++ b/yack-app/pagesA/home/device/statistic/history.vue @@ -81,7 +81,7 @@ import uniPagination from '@/pagesA/components/uni-pagination/index.vue'; import { getSubGatewayList } from '@/apis/modules/gateway.js'; import { listThingsModel } from '@/apis/modules/device.js'; - import { getHistoryList, getSubDeviceComparativeHistory } from '@/apis/modules/deviceLog.js'; + import { getHistoryList, getSubDeviceComparativeHistory, getDeviceHistoryCalculated } from '@/apis/modules/deviceLog.js'; export default { name: 'deviceHistory', @@ -364,6 +364,8 @@ text: item.modelName, value: item.identifier, type: item.type, + isCalculated: item.isCalculated, // 是否计算物模型 + slaveId: item.slaveId, // 子设备从机号(网关自身物模型为空) } } }).filter(item => item !== undefined); @@ -397,22 +399,10 @@ const { deviceId, serialNumber, deviceType } = this.deviceInfo; if (identifiers.length !== 0) { this.historys = []; + const beginTimeStr = moment(beginTime).format('YYYY-MM-DD HH:mm:ss'); + const endTimeStr = moment(endTime).format('YYYY-MM-DD HH:mm:ss'); if (deviceType === 2) { - // 网关设备:调用多设备对比接口,数据按子设备SN存储 - const params = { - gwDeviceId: deviceId, - identifierList: identifiers, - type: 1, - beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'), - endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'), - }; - getSubDeviceComparativeHistory(params).then(res => { - if (res.code === 200) { - this.historys = res.data || []; - this.chartData = this.getChartDatas(); - this.getTableDatas(); - } - }) + this.getGatewayHistory(identifiers, deviceId, serialNumber, beginTimeStr, endTimeStr); } else { // 直连设备 const idenList = identifiers.map((item) => { @@ -424,8 +414,8 @@ }); const params = { identifierList: idenList, - beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'), - endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'), + beginTime: beginTimeStr, + endTime: endTimeStr, deviceId: deviceId, serialNumber: serialNumber, }; @@ -439,14 +429,115 @@ } } }, + // 网关设备历史查询:普通变量走多设备对比接口,计算物模型走计算接口,结果合并 + getGatewayHistory (identifiers, deviceId, serialNumber, beginTimeStr, endTimeStr) { + // 计算物模型:isCalculated=1 且属于网关自身产品(无slaveId) + const calculatedItems = identifiers.filter((id) => { + const iden = this.identifierList.find((chil) => chil.value === id); + return iden && iden.isCalculated === 1 && (iden.slaveId === null || iden.slaveId === undefined || iden.slaveId === ''); + }); + const normalIdentifiers = identifiers.filter((id) => !calculatedItems.includes(id)); + + const tasks = []; + if (normalIdentifiers.length > 0) { + // 普通变量:多设备对比接口(数据按子设备SN存储) + tasks.push(getSubDeviceComparativeHistory({ + gwDeviceId: deviceId, + identifierList: normalIdentifiers, + type: 1, + beginTime: beginTimeStr, + endTime: endTimeStr, + }).then(res => ({ + type: 'compare', + data: res.code === 200 ? (res.data || []) : [] + }))); + } + if (calculatedItems.length > 0) { + // 计算物模型:查询时按公式计算 + const idenList = calculatedItems.map((id) => { + const iden = this.identifierList.find((chil) => chil.value === id); + return { + identifier: iden.value, + type: iden.type + }; + }); + tasks.push(getDeviceHistoryCalculated({ + identifierList: idenList, + beginTime: beginTimeStr, + endTime: endTimeStr, + deviceId: deviceId, + serialNumber: serialNumber, + }).then(res => ({ + type: 'calculated', + data: res.code === 200 ? (res.data || []) : [] + }))); + } + if (tasks.length === 0) { + return; + } + + Promise.all(tasks).then((results) => { + // 统一转换为 [{ time, 列名: 值 }] 格式后按时间合并 + const rowMap = new Map(); + results.forEach((result) => { + if (result.type === 'compare') { + // subDeviceComparativeHistory 格式: [{ "time": "...", "子设备A(从机1)": "25.5" }] + result.data.forEach((row) => { + if (!row.time) { + return; + } + if (!rowMap.has(row.time)) { + rowMap.set(row.time, { time: row.time }); + } + const target = rowMap.get(row.time); + Object.keys(row).forEach((k) => { + if (k !== 'time') { + target[k] = row[k]; + } + }); + }); + } else { + // deviceHistoryCalculated 格式: [{ "2026-08-23 10:00:00": [{ "calcVar": "12.3" }] }] + result.data.forEach((item) => { + const time = Object.keys(item)[0]; + if (!time) { + return; + } + if (!rowMap.has(time)) { + rowMap.set(time, { time }); + } + const target = rowMap.get(time); + Object.values(item)[0].forEach((chil) => { + const id = Object.keys(chil)[0]; + const iden = this.identifierList.find((m) => m.value === id); + // 计算物模型列名使用变量名 + target[iden ? iden.text : id] = Object.values(chil)[0]; + }); + }); + } + }); + // 按时间升序排序 + this.historys = Array.from(rowMap.values()).sort((a, b) => (a.time < b.time ? -1 : 1)); + this.chartData = this.getChartDatas(); + this.getTableDatas(); + }); + }, // 获取折线图数据(适配两种接口响应格式) getChartDatas () { const { deviceType } = this.deviceInfo; if (deviceType === 2) { - // subDeviceComparativeHistory 格式: [{ "time": "2026-08-23 10:00:00", "子设备A": "25.5", "子设备B": "26.0" }] + // 网关:合并后的格式 [{ "time": "...", "子设备A(从机1)": "25.5", "计算变量": "12.3" }] + if (this.historys.length === 0) { + return { categories: [], series: [] }; + } const categories = this.historys.map((item) => item.time); - // 获取除 time 外的所有列名作为系列 - const keys = Object.keys(this.historys[0] || {}).filter(k => k !== 'time'); + // 从所有行收集列名(避免第一行缺少某些列) + const keys = []; + this.historys.forEach((item) => Object.keys(item).forEach((k) => { + if (k !== 'time' && !keys.includes(k)) { + keys.push(k); + } + })); const series = keys.map((key) => { return { name: key, @@ -473,8 +564,14 @@ getTableDatas () { const { deviceType } = this.deviceInfo; if (deviceType === 2) { - // subDeviceComparativeHistory 格式 - const keys = Object.keys(this.historys[0] || {}).filter(k => k !== 'time'); + // 网关:合并后的格式 [{ "time": "...", "子设备A(从机1)": "25.5", "计算变量": "12.3" }] + // 从所有行收集列名(避免第一行缺少某些列) + const keys = []; + this.historys.forEach((item) => Object.keys(item).forEach((k) => { + if (k !== 'time' && !keys.includes(k)) { + keys.push(k); + } + })); this.tableHeaders = keys.map((key) => ({ name: key, value: key, From cb00505ae798195c5dc17965cf7baf69af0bf3a3 Mon Sep 17 00:00:00 2001 From: zhugongping Date: Wed, 2 Sep 2026 09:27:43 +0800 Subject: [PATCH 04/19] =?UTF-8?q?20260902:=E8=A7=84=E5=88=99=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=94=B9=E9=80=A0=EF=BC=8C=E6=94=AF=E6=8C=81=E7=B2=BE?= =?UTF-8?q?=E7=A1=AE=E5=88=B0=E8=AE=BE=E5=A4=87(=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E8=AE=BE=E5=A4=87);mqtt=E6=94=AF=E6=8C=81=E9=80=9A=E9=85=8D?= =?UTF-8?q?=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/fastbee/iot/domain/Device.java | 7 +++ .../java/com/fastbee/iot/domain/Product.java | 2 +- .../java/com/fastbee/iot/domain/Script.java | 5 ++ .../fastbee/iot/model/ScriptCondition.java | 5 ++ .../fastbee/iot/ruleEngine/MqttBridge.java | 2 +- .../fastbee/iot/ruleEngine/RuleProcess.java | 1 + .../resources/mapper/iot/DeviceMapper.xml | 12 ++++ .../resources/mapper/iot/ScriptMapper.xml | 22 +++++-- sql/yack-init.sql | 1 + .../src/views/iot/device/product-list.vue | 2 +- yack-web/src/views/iot/scene/device-list.vue | 11 +--- yack-web/src/views/iot/scene/product-list.vue | 2 +- yack-web/src/views/iot/scene/script.vue | 57 +++++++++++++++++++ 13 files changed, 110 insertions(+), 19 deletions(-) diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java index acecb3a7..79d45989 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java @@ -101,6 +101,13 @@ public class Device extends BaseEntity { @TableField(exist = false) private Integer deviceType; + /** + * 设备类型列表(用于多类型过滤) + */ + @ApiModelProperty("设备类型列表(用于多类型过滤,如[1,2]表示直连和网关设备)") + @TableField(exist = false) + private List deviceTypes; + /** * 设备状态(1-未激活,2-禁用,3-在线,4-离线) */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java index 43aea31b..79318b81 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Product.java @@ -115,7 +115,7 @@ public class Product extends BaseEntity { /** * 设备类型(1-直连设备、2-网关子设备、3-网关设备) */ - @ApiModelProperty(value = "设备类型", notes = "(1-直连设备、2-网关子设备、3-网关设备、4-网关子设备)") + @ApiModelProperty(value = "设备类型", notes = "(1-直连设备、2-网关设备、3-监控设备、4-网关子设备)") @Excel(name = "设备类型", readConverterExp = "1=直连设备、2=网关设备、3=监控设备、4-网关子设备") private Integer deviceType; /** diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java index 376dfcd7..f8cf1f4c 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Script.java @@ -96,6 +96,11 @@ public class Script extends BaseEntity { private String productName; + /** + * 设备编号列表(逗号分隔,为空表示全部设备) + */ + private String serialNumbers; + /** * 是否生效 */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java index 626dedf2..85f5642b 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ScriptCondition.java @@ -46,6 +46,11 @@ public class ScriptCondition { */ private Long productId; + /** + * 当前上报设备的编号(用于设备级过滤,为空不过滤) + */ + private String serialNumber; + /** * 场景ID */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java index 214c66b4..a5ab512e 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java @@ -44,7 +44,7 @@ public class MqttBridge extends NodeComponent { // 优先使用bridge.route,如果为空再使用context.topic String publishTopic = StringUtils.isNotBlank(bridge.getRoute()) - ? bridge.getRoute() + ? cxt.placeholders(bridge.getRoute()) : cxt.getTopic(); MqttMessage message = new MqttMessage(); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/RuleProcess.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/RuleProcess.java index c2ecc79b..e49709dc 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/RuleProcess.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/RuleProcess.java @@ -49,6 +49,7 @@ public class RuleProcess { .scriptPurpose(1) .scriptEvent(event) .productId(productCode.getProductId()) + .serialNumber(serialNumber) .build(); MsgContext context = MsgContext.builder() .serialNumber(serialNumber) diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml index a6ba4d5a..235adc80 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml @@ -443,6 +443,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and d.device_name like concat('%', #{deviceName}, '%') and d.product_id = #{productId} and p.device_type = #{deviceType} + + and p.device_type in + + #{deviceTypeItem} + + and d.product_name like concat('%', #{productName}, '%') and d.serial_number = #{serialNumber} and d.gw_dev_code = #{gwDevCode} @@ -1002,6 +1008,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and d.device_name like concat('%', #{deviceName}, '%') and d.product_id = #{productId} and p.device_type = #{deviceType} + + and p.device_type in + + #{deviceTypeItem} + + and d.product_name like concat('%', #{productName}, '%') and d.serial_number = #{serialNumber} and d.gw_dev_code = #{gwDevCode} diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml index 4bd5373c..bb833cdc 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ScriptMapper.xml @@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -32,16 +33,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select user_id, user_name, scene_id,product_id,product_name,script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_data, script_type, script_language,enable, del_flag, create_by, create_time, update_by, update_time, remark from iot_script + select user_id, user_name, scene_id,product_id,product_name,serial_numbers,script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_data, script_type, script_language,enable, del_flag, create_by, create_time, update_by, update_time, remark from iot_script - select user_id, user_name, scene_id,product_id,product_name, script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_type, script_language,enable, create_by, create_time, update_by, update_time from iot_script + select user_id, user_name, scene_id,product_id,product_name,serial_numbers, script_event,script_action,script_purpose,script_order,application_name, script_id, script_name, script_type, script_language,enable, create_by, create_time, update_by, update_time from iot_script @@ -106,7 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + DELETE FROM iot_event_log + WHERE create_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java index c68e4946..d9397f81 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysLogininforMapper.java @@ -41,4 +41,12 @@ public interface SysLogininforMapper { public int cleanLogininfor(); public List getUserCount(); + + /** + * 删除指定天数前的登录日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + public int deleteLogininforBeforeDays(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java index 46ac307e..a05e33b6 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysOperLogMapper.java @@ -45,4 +45,12 @@ public interface SysOperLogMapper { * 清空操作日志 */ public void cleanOperLog(); + + /** + * 删除指定天数前的操作日志 + * + * @param days 保留天数 + * @return 删除记录数 + */ + public int deleteOperLogBeforeDays(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java index ef1fcb69..7c27865a 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysLogininforService.java @@ -42,4 +42,11 @@ public interface ISysLogininforService { * 统计一周内的登陆数据 */ public List getUserCount(); + + /** + * 清理指定天数前的登录日志 + * + * @param days 保留天数 + */ + public void cleanLogininfor(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java index b7713433..f2b21e28 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysOperLogService.java @@ -45,4 +45,11 @@ public interface ISysOperLogService { * 清空操作日志 */ public void cleanOperLog(); + + /** + * 清理指定天数前的操作日志 + * + * @param days 保留天数 + */ + public void cleanOperLog(int days); } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java index 3d8cd3d3..e17fdf6f 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysLogininforServiceImpl.java @@ -63,4 +63,14 @@ public class SysLogininforServiceImpl implements ISysLogininforService { public List getUserCount() { return logininforMapper.getUserCount(); } + + /** + * 清理指定天数前的登录日志 + * + * @param days 保留天数 + */ + @Override + public void cleanLogininfor(int days) { + logininforMapper.deleteLogininforBeforeDays(days); + } } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java index 5bfed0ee..f0ace9a1 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysOperLogServiceImpl.java @@ -68,4 +68,14 @@ public class SysOperLogServiceImpl implements ISysOperLogService { public void cleanOperLog() { operLogMapper.cleanOperLog(); } + + /** + * 清理指定天数前的操作日志 + * + * @param days 保留天数 + */ + @Override + public void cleanOperLog(int days) { + operLogMapper.deleteOperLogBeforeDays(days); + } } diff --git a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml index 3fe4ccd7..d5ecdf38 100644 --- a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml +++ b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -76,4 +76,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) b on a.login_time = b.datetime; + + DELETE FROM sys_logininfor + WHERE login_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + diff --git a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml index 38456ed0..13a93f56 100644 --- a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml +++ b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -80,4 +80,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" truncate table sys_oper_log + + DELETE FROM sys_oper_log + WHERE oper_time < DATE_SUB(NOW(), INTERVAL #{days} DAY) + + \ No newline at end of file diff --git a/sql/log_clean_task_init.sql b/sql/log_clean_task_init.sql new file mode 100644 index 00000000..36e0efb2 --- /dev/null +++ b/sql/log_clean_task_init.sql @@ -0,0 +1,96 @@ +-- ============================================ +-- 日志清理定时任务初始化 SQL +-- ============================================ + +-- 1. 插入定时任务记录到 sys_job 表 +-- 说明:每天凌晨 0 点自动清理 15 天前的日志数据 +-- 注意:LogCleanTask 类位于 fastbee-admin 模块的 com.fastbee.web.task 包下 +INSERT INTO `sys_job` ( + `job_name`, + `job_group`, + `invoke_target`, + `cron_expression`, + `misfire_policy`, + `concurrent`, + `status`, + `create_by`, + `create_time`, + `remark` +) VALUES ( + '日志清理任务', -- 任务名称 + 'SYSTEM', -- 任务组名 + 'logCleanTask.cleanLogs()', -- 调用目标字符串 + '0 0 0 * * ?', -- Cron 表达式:每天凌晨 00:00:00 + '3', -- 错误策略:不触发立即执行 + '1', -- 禁止并发执行 + '0', -- 状态:正常 + 'admin', -- 创建者 + NOW(), -- 创建时间 + '每天凌晨0点自动清理15天前的日志数据(iot_event_log、sys_oper_log、sys_logininfor、iot_alert_log、sys_job_log、notify_log)' +); + +-- ============================================ +-- 清理的日志表说明 +-- ============================================ +-- 1. iot_event_log - IoT 事件日志 +-- 2. sys_oper_log - 系统操作日志 +-- 3. sys_logininfor - 登录日志 +-- 4. iot_alert_log - 设备告警日志 +-- 5. sys_job_log - 定时任务日志 +-- 6. notify_log - 通知日志 +-- +-- 注意:iot_device_log 表(设备监测日志)不参与清理 +-- 该表为核心数据,目前有 214 万条记录,需要数据迁移方案 + +-- 2. 可选:添加日志清理权限菜单(根据需要执行) +-- 如果需要在管理后台显示权限按钮,可以执行以下 SQL +/* +INSERT INTO `sys_menu` ( + `menu_name`, + `parent_id`, + `order_num`, + `path`, + `component`, + `query`, + `is_frame`, + `is_cache`, + `menu_type`, + `visible`, + `status`, + `perms`, + `icon`, + `create_by`, + `create_time`, + `remark` +) VALUES ( + '日志清理', -- 菜单名称 + 2, -- 父菜单ID(根据实际情况调整,2通常是系统管理) + 99, -- 显示顺序 + '', -- 路由地址 + '', -- 组件路径 + '', -- 路由参数 + 1, -- 不是外链 + 0, -- 不缓存 + 'F', -- 菜单类型:F=按钮 + '0', -- 显示状态 + '0', -- 菜单状态 + 'system:log:clean', -- 权限标识 + '#', -- 图标 + 'admin', -- 创建者 + NOW(), -- 创建时间 + '日志清理按钮权限' +); +*/ + +-- ============================================ +-- 验证定时任务是否创建成功 +-- ============================================ +-- SELECT * FROM sys_job WHERE job_name = '日志清理任务'; + +-- ============================================ +-- 手动触发清理(可选) +-- ============================================ +-- 如果需要立即清理历史数据,可以通过以下方式: +-- 方式1:通过管理后台 -> 系统监控 -> 定时任务 -> 点击"执行一次" +-- 方式2:调用 API 接口:POST /system/log/clean?days=30 +-- 方式3:数据库执行:UPDATE sys_job SET status = '0' WHERE job_name = '日志清理任务'; \ No newline at end of file From 2ae8b8fab3b8fe707e9c59bb34beafab897caf76 Mon Sep 17 00:00:00 2001 From: zhugongping Date: Sun, 13 Sep 2026 16:02:08 +0800 Subject: [PATCH 10/19] =?UTF-8?q?=E8=B0=83=E6=95=B4docker=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=E6=97=A5=E5=BF=97=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index fa154571..ed2c0423 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -72,7 +72,7 @@ services: logging: driver: "json-file" options: - max-size: "10m" + max-size: "50m" max-file: "2" ports: - 8080:8080 From 6963136775ff3049acf978b9d918f273833c1b27 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 15 Sep 2026 09:10:56 +0800 Subject: [PATCH 11/19] =?UTF-8?q?20260915=EF=BC=9A=E4=B8=B4=E6=97=B681?= =?UTF-8?q?=E5=BF=83=E8=B7=B3=E5=8C=85=E5=BD=93=E6=B3=A8=E5=86=8C=E5=8C=85?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/fastbee/modbus/model/ModbusRtu.java | 2 +- .../main/java/com/fastbee/modbus/pak/ModbusEndPoint.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java index 32dcb007..c87d6283 100644 --- a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java +++ b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/model/ModbusRtu.java @@ -47,7 +47,7 @@ public class ModbusRtu extends Message { protected int start; @Column(length = 1, version = {0x80, 0x81}, desc = "标识位,实例设备是0x80") protected int mId; - @Column(length = 6, version = 0x80, desc = "MAC地址,6个字节") + @Column(length = 6, version = {0x80, 0x81}, desc = "MAC地址,6个字节") protected String mac; @Column(length = 1, version = {0x80, 0x81}, desc = "结尾,如:7E") protected int end; diff --git a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java index aad04a54..417c0efb 100644 --- a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java +++ b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/pak/ModbusEndPoint.java @@ -39,7 +39,14 @@ public class ModbusEndPoint { @PakMapping(types = 心跳包) public void heartbeat(DeviceReport message, Session session) { - + //心跳包处理:确保设备注册并更新在线状态 + if (!session.isRegistered()) { + session.register(message); + //记录设备信息 + DeviceMsg deviceMsg = new DeviceMsg(); + deviceMsg.setClientId(message.getClientId()); + session.setAttribute(SessionKey.DeviceMsg, deviceMsg); + } } } From d7a524cba34ecab8e8212806424e3d95a982849c Mon Sep 17 00:00:00 2001 From: zhugongping Date: Tue, 15 Sep 2026 15:25:50 +0800 Subject: [PATCH 12/19] =?UTF-8?q?20260915:traceId=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge-base/07-TraceId 链路追踪实现.md | 1213 +++++++++++++++++ .../com/fastbee/common/config/DeviceTask.java | 13 +- .../common/constant/FastBeeConstant.java | 2 + .../fastbee/common/core/mq/DeviceReplyBo.java | 3 + .../common/core/mq/DeviceReportBo.java | 3 + .../common/core/mq/DeviceStatusBo.java | 4 + .../common/core/mq/DeviceTestReportBo.java | 3 + .../fastbee/common/core/mq/InvokeReqDto.java | 3 + .../common/core/mq/MQSendMessageBo.java | 3 + .../common/core/mq/ota/OtaUpgradeBo.java | 3 + .../core/mq/ota/OtaUpgradeDelayTask.java | 5 + .../common/trace/MdcTaskDecorator.java | 75 + .../fastbee/common/trace/TraceContext.java | 270 ++++ .../common/trace/TraceIdGenerator.java | 106 ++ .../fastbee/common/utils/TraceIdUtils.java | 184 +-- .../aspectj/ScheduledTraceAspect.java | 47 + .../framework/config/OtaThreadPoolConfig.java | 5 + .../framework/config/ThreadPoolConfig.java | 5 + .../interceptor/TraceIdInterceptor.java | 22 +- .../iot/data/job/DeviceAbstractQuartzJob.java | 13 +- .../iot/data/listen/DeviceOtherListen.java | 11 +- .../data/listen/DevicePropFetchListen.java | 14 +- .../iot/data/listen/DeviceReplyListen.java | 13 +- .../iot/data/listen/DeviceReportListen.java | 16 +- .../iot/data/listen/DeviceStatusListen.java | 13 +- .../iot/data/listen/DeviceTestListen.java | 10 +- .../iot/data/listen/FunctionInvokeListen.java | 11 +- .../iot/data/listen/UpGradeListener.java | 15 +- .../iot/data/listen/UpgradeListen.java | 15 +- .../service/impl/FirmwareTaskServiceImpl.java | 5 +- .../fastbee/mq/producer/MessageProducer.java | 45 + .../quartz/util/AbstractQuartzJob.java | 11 +- .../record/config/ThreadPoolTaskConfig.java | 5 + .../base/core/hanler/TraceHandler.java | 38 + .../java/com/fastbee/server/TCPServer.java | 5 +- .../java/com/fastbee/server/UDPServer.java | 3 + .../server/handler/DispatcherHandler.java | 13 +- .../handler/adapter/MqttMessageDelegate.java | 2 + .../com/fastbee/mqtt/server/MqttServer.java | 3 + .../fastbee/mqtt/server/WebSocketServer.java | 3 + .../iot/model/modbus/ModbusPollJob.java | 3 + sql/log_clean_task_init.sql | 2 +- 42 files changed, 2044 insertions(+), 194 deletions(-) create mode 100644 docs/knowledge-base/07-TraceId 链路追踪实现.md create mode 100644 fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java create mode 100644 fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java create mode 100644 fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java create mode 100644 fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java create mode 100644 fastbee-server/base-server/src/main/java/com/fastbee/base/core/hanler/TraceHandler.java diff --git a/docs/knowledge-base/07-TraceId 链路追踪实现.md b/docs/knowledge-base/07-TraceId 链路追踪实现.md new file mode 100644 index 00000000..56095386 --- /dev/null +++ b/docs/knowledge-base/07-TraceId 链路追踪实现.md @@ -0,0 +1,1213 @@ +# 任务:TraceId 链路追踪与上下文传播能力 + +你现在需要为我的 Java 单体项目实现一套“统一 TraceId 链路追踪基础设施”。 + +## 一、项目背景 + +当前项目是 Java 单体应用,技术栈: + +- JDK 8 +- Spring Boot 2.5.x +- Spring MVC +- Logback / SLF4J +- MySQL +- Redis +- Quartz +- MQ +- Netty +- MQTT +- 后续会接入 Python AI 服务、LLM、微服务、OpenTelemetry + +项目未来会逐步发展为: + +```text +单体应用 + ↓ +IoT + SaaS + AI + ↓ +微服务 + ↓ +OpenTelemetry / 分布式链路追踪 +``` + +因此这次实现不能只是简单地增加一个 MDC 字段,而要设计成可长期演进的基础框架。 + +--- + +# 二、核心目标 + +实现统一的: + +```text +TraceContext +``` + +核心字段至少包括: + +```text +traceId +tenantId +userId +``` + +其中当前阶段最重要的是: + +```text +traceId +``` + +核心原则: + +> 一次完整业务执行链使用同一个 traceId。 + +例如: + +```text +前端请求 + ↓ +Controller + ↓ +Service + ↓ +线程池 + ↓ +MQ + ↓ +MQ Consumer + ↓ +AI 服务 + ↓ +数据库 + ↓ +通知 +``` + +如果它们属于同一次业务执行,则全部使用: + +```text +traceId = A +``` + +--- + +# 三、TraceId 生命周期规则 + +必须严格遵循以下规则。 + +## 1. 有上游 TraceId + +优先继承。 + +例如 HTTP: + +```http +X-Trace-Id: 01KABC... +``` + +后端收到后: + +```text +TraceId = 01KABC... +``` + +不要重新生成。 + +--- + +## 2. 没有上游 TraceId + +自动创建新的 TraceId。 + +例如: + +```text +浏览器 + ↓ +HTTP + ↓ +Java +``` + +如果 HTTP 没有: + +```http +X-Trace-Id +``` + +则 Java 自动生成。 + +--- + +## 3. 定时任务没有上游 TraceId + +Quartz / @Scheduled 等任务启动时自动生成: + +```text +traceId +``` + +例如: + +```text +traceId=T202609150001 +``` + +整个任务执行过程保持相同 TraceId。 + +--- + +# 四、TraceId 生成器 + +设计独立: + +```java +TraceIdGenerator +``` + +不要把 TraceId 生成逻辑写死在 Filter 中。 + +要求: + +```text +TraceContext +TraceIdGenerator +``` + +职责分离。 + +优先考虑 ULID 或项目中已有的高质量 ID 生成方案。 + +要求: + +- 唯一 +- 高并发安全 +- 尽量时间有序 +- 适合日志检索 +- 不依赖数据库 +- 后续可以替换实现 + +--- + +# 五、TraceContext 设计 + +创建统一: + +```java +TraceContext +``` + +例如: + +```text +TraceContext +├── getTraceId() +├── setTraceId() +├── getOrCreateTraceId() +├── set() +├── get() +├── clear() +└── snapshot() +``` + +不要让业务代码直接到处: + +```java +MDC.put("traceId", ...) +``` + +业务代码应该依赖: + +```java +TraceContext +``` + +MDC 只是 TraceContext 的一种底层日志传播实现。 + +核心原则: + +```text +TraceContext ≠ MDC +``` + +而应该是: + +```text +TraceContext + ↓ +MDC +HTTP Header +MQ Header +ThreadPool +Future +``` + +--- + +# 六、Web 请求 + +实现 Spring MVC Filter。 + +建议: + +```java +OncePerRequestFilter +``` + +功能: + +1. 从 HTTP Header 获取: + +```http +X-Trace-Id +``` + +2. 如果存在且合法: + +```text +继续使用 +``` + +3. 如果不存在: + +```text +生成 TraceId +``` + +4. 放入: + +```text +TraceContext +MDC +``` + +5. 将 TraceId 写入 HTTP Response Header: + +```http +X-Trace-Id: 01KABC... +``` + +6. 请求结束后必须清理: + +```text +MDC +TraceContext +``` + +防止 Tomcat 线程池复用造成 TraceId 污染。 + +--- + +# 七、日志输出 + +修改 Logback 日志格式,使所有日志自动包含: + +```text +traceId +``` + +例如: + +```text +2026-09-15 11:20:31.123 INFO +[http-nio-8080-exec-5] +traceId=01KABC123 +DeviceService +- +查询设备成功 deviceId=DEV001 +``` + +要求: + +- 控制台日志包含 traceId +- 普通文件日志包含 traceId +- error 日志包含 traceId +- 不要求业务代码修改每一条 log.info() +- 使用 MDC 自动完成 + +同时考虑 JSON 日志扩展能力。 + +--- + +# 八、线程池上下文传播 + +这是本任务的重点。 + +普通: + +```java +ThreadPoolExecutor +``` + +或者: + +```java +CompletableFuture +``` + +不能保证 MDC 自动传播。 + +必须实现统一的上下文传播机制。 + +例如: + +```text +TraceContext + ↓ +snapshot + ↓ +Runnable / Callable + ↓ +restore + ↓ +执行 + ↓ +clear +``` + +要求实现类似: + +```text +TraceContextRunnable +TraceContextCallable +TraceableExecutor +``` + +或者使用 Spring 提供的: + +```text +TaskDecorator +``` + +如果项目使用 Spring ThreadPoolTaskExecutor,优先通过: + +```text +TaskDecorator +``` + +实现。 + +目标: + +```text +Web线程 +traceId=A + ↓ +线程池 +traceId=A +``` + +例如: + +```java +executor.execute(() -> { + log.info("异步处理"); +}); +``` + +日志必须仍然是: + +```text +traceId=A +``` + +--- + +# 九、CompletableFuture + +重点处理: + +```java +CompletableFuture.runAsync(...) +CompletableFuture.supplyAsync(...) +thenApply(...) +thenAccept(...) +thenRun(...) +``` + +分析项目中现有异步代码。 + +不要简单粗暴地修改所有业务代码。 + +优先通过统一线程池: + +```text +TraceableExecutor +``` + +解决。 + +如果无法完全覆盖 CompletableFuture 的默认线程池,需要明确指出风险,并提供统一解决方案。 + +禁止让业务代码到处手动: + +```java +MDC.put(...) +MDC.clear(...) +``` + +--- + +# 十、MQ TraceId 传播 + +这是第二个重点。 + +当 Java 发送 MQ 消息时: + +必须把当前: + +```text +traceId +``` + +写入 MQ Message Header / Properties。 + +例如: + +```text +traceId=A +``` + +发送: + +```text +MQ Message +├── Header +│ └── traceId=A +└── Body +``` + +Consumer 收到消息: + +```text +MQ Header + ↓ +读取 traceId + ↓ +TraceContext.setTraceId() + ↓ +MDC + ↓ +业务处理 +``` + +这样: + +```text +Web +traceId=A + ↓ +MQ Producer +traceId=A + ↓ +MQ +traceId=A + ↓ +Consumer +traceId=A +``` + +如果 MQ 消息没有 TraceId: + +```text +Consumer + ↓ +自动生成新的 TraceId +``` + +但正常情况下必须继承 Producer 的 TraceId。 + +--- + +# 十一、MQ 不同消息中间件 + +先检查当前项目实际使用的 MQ。 + +不要凭空假设。 + +如果项目目前只有一种 MQ: + +优先实现当前实际使用的 MQ。 + +同时将设计抽象成: + +```text +TraceMessageInterceptor +``` + +未来可以支持: + +```text +RabbitMQ +Kafka +RocketMQ +Redis Stream +其他 MQ +``` + +不要为了扩展性一次性实现大量没有使用的 MQ。 + +--- + +# 十二、HTTP 调用链 + +如果 Java 单体项目会调用其他服务: + +例如: + +```text +Java + ↓ HTTP +Python AI +``` + +必须自动传递: + +```http +X-Trace-Id: A +``` + +如果项目使用: + +```text +RestTemplate +WebClient +OpenFeign +HttpClient +OkHttp +``` + +检查实际使用情况。 + +优先通过统一 Interceptor 实现。 + +例如: + +```text +Java +traceId=A + ↓ +HTTP Header +X-Trace-Id=A + ↓ +Python AI +traceId=A +``` + +这样 Java 和 Python 日志可以使用同一个 TraceId 检索。 + +--- + +# 十三、Netty / MQTT / TCP / UDP + +这是 IoT 项目的重要场景。 + +检查项目现有: + +```text +Netty +MQTT +TCP +UDP +CoAP +``` + +入口。 + +对于每一个独立的消息 / 请求: + +如果存在上游 TraceId: + +```text +继承 +``` + +否则: + +```text +生成 +``` + +然后进入: + +```text +TraceContext +``` + +例如: + +```text +设备 DEV001 + ↓ +MQTT + ↓ +traceId=M001 + ↓ +数据解析 + ↓ +物模型 + ↓ +数据库 + ↓ +告警 +``` + +整条处理链使用: + +```text +traceId=M001 +``` + +注意: + +> 设备每次独立上报消息属于独立业务事件,因此通常应该生成新的 TraceId,而不是让一个设备永久使用同一个 TraceId。 + +--- + +# 十五、SaaS 上下文 + +项目未来是 SaaS,因此 TraceContext 预留: + +```text +traceId +tenantId +userId +``` + +如果当前项目已经有: + +```text +tenantId +userId +``` + +则在请求认证成功后自动放入上下文。 + +例如: + +```text +traceId=A +tenantId=10001 +userId=9527 +``` + +但不要因为本次任务而大范围修改现有权限系统。 + +只需要提供统一上下文承载能力。 + +--- + +# 十六、线程安全 + +这是重点。 + +必须考虑: + +```text +Tomcat线程池复用 +Quartz线程池 +业务线程池 +CompletableFuture +MQ Consumer线程 +Netty EventLoop +``` + +任何任务结束后都必须: + +```text +clear +``` + +禁止出现: + +```text +请求A +traceId=A + +请求结束 + +请求B +traceId=A +``` + +这种严重的数据串链问题。 + +--- + +# 十七、异常处理 + +异常情况下 TraceId 也必须保留。 + +例如: + +```text +traceId=A +ERROR +设备数据处理失败 +``` + +完整异常堆栈必须能够通过: + +```text +traceId=A +``` + +检索出来。 + +Web 异常响应最好也返回: + +```http +X-Trace-Id: A +``` + +这样前端报错: + +```text +TraceId: A +``` + +我就可以直接去 Kibana / 日志文件搜索: + +```text +traceId=A +``` + +定位问题。 + +--- + +# 十八、日志检索目标 + +最终必须支持: + +```bash +grep "traceId=01KABC" app.log +``` + +或者 Kibana: + +```text +traceId:"01KABC" +``` + +一次找到: + +```text +HTTP请求 +Service +数据库 +线程池 +MQ Producer +MQ Consumer +AI +异常 +``` + +--- + +# 十九、代码结构要求 + +根据当前项目实际结构,合理放置。 + +如果存在类似: + +```text +common +framework +core +starter +``` + +优先放到公共基础模块。 + +推荐结构: + +```text +xxx-core +└── trace + ├── TraceContext + ├── TraceIdGenerator + ├── TraceContextSnapshot + ├── TraceContextRunnable + └── TraceContextCallable + +xxx-web-starter +└── trace + └── TraceIdFilter + +xxx-log-starter +└── trace + └── MDC / Logback + +xxx-schedule-starter +└── trace + └── Quartz Trace + +xxx-mq-starter +└── trace + ├── Producer Trace + └── Consumer Trace +``` + +具体目录必须根据当前项目实际结构调整,不要机械套用。 + +--- + +# 二十、不要过度设计 + +当前项目是单体项目。 + +第一阶段: + +必须实现: + +```text +TraceId +MDC +Web +ThreadPool +Quartz +MQ +HTTP +``` + +暂时不要自行实现完整: + +```text +Span +Trace Graph +Jaeger +Zipkin +SkyWalking +OpenTelemetry Collector +``` + +但是代码设计要为未来 OpenTelemetry 留出扩展点。 + +也就是说: + +```text +现在: +TraceId + MDC + +未来: +TraceId + SpanId + ParentSpanId + OpenTelemetry +``` + +不要把当前 TraceContext 设计成未来无法迁移的死结构。 + +--- + +# 二十一、需要先做代码分析 + +在开始修改代码之前: + +1. 扫描整个项目结构。 +2. 找到 Spring Boot 启动模块。 +3. 找到 Web 请求入口。 +4. 找到现有 Filter / Interceptor。 +5. 找到现有线程池。 +6. 找到 CompletableFuture。 +7. 找到 Quartz / @Scheduled。 +8. 找到 MQ Producer。 +9. 找到 MQ Consumer。 +10. 找到 RestTemplate / Feign / HTTP Client。 +11. 找到 Netty / MQTT / TCP / UDP 入口。 +12. 找到 Logback 配置。 +13. 找到现有 MDC / ThreadLocal / RequestContext 实现。 +14. 检查项目是否已经存在类似 TraceId / RequestId 机制。 + +如果已经存在类似能力: + +> 优先重构和统一,不要重复造轮子。 + +--- + +# 二十二、实现原则 + +遵守: + +```text +低侵入 +高复用 +自动传播 +自动清理 +统一入口 +统一出口 +``` + +业务代码尽可能不需要修改。 + +理想效果: + +原来的代码: + +```java +log.info("开始处理设备"); +``` + +完全不用修改。 + +自动变成: + +```text +traceId=A 开始处理设备 +``` + +--- + +# 二十三、测试要求 + +实现完成后必须增加测试。 + +至少测试: + +### Test 1:HTTP + +```text +请求进入 +↓ +生成 TraceId +↓ +Controller +↓ +Service +↓ +日志 +↓ +Response Header +``` + +确认 TraceId 一致。 + +### Test 2:HTTP携带 TraceId + +```text +X-Trace-Id=A +``` + +确认后端仍然使用: + +```text +A +``` + +### Test 3:线程池 + +```text +Web traceId=A +↓ +ThreadPool +``` + +确认: + +```text +traceId=A +``` + +### Test 4:MQ + +```text +Producer traceId=A +↓ +MQ Header +↓ +Consumer +``` + +确认 Consumer: + +```text +traceId=A +``` + +### Test 5:MQ无 TraceId + +确认 Consumer 自动生成。 + +### Test 6:定时任务 + +确认: + +```text +TASK traceId=A +``` + +整个任务一致。 + +### Test 7:线程池污染 + +执行: + +```text +请求A +请求B +请求C +``` + +确认: + +```text +A ≠ B ≠ C +``` + +并且不会出现线程复用导致 TraceId 串线。 + +### Test 8:异常 + +异常发生后: + +```text +traceId +``` + +仍然存在于 ERROR 日志。 + +### Test 9:并发 + +同时执行大量请求: + +```text +100+ +1000+ +``` + +确认 TraceId 不串线。 + +--- + +# 二十四、最终输出要求 + +完成后请给我: + +## 1. 修改文件列表 + +例如: + +```text +新增: +xxx/TraceContext.java + +修改: +xxx/Logback.xml +xxx/xxxFilter.java +``` + +## 2. 架构说明 + +说明: + +```text +Web + ↓ +TraceContext + ↓ +MDC + +ThreadPool + ↓ +Context Propagation + +MQ + ↓ +Message Header + +HTTP + ↓ +X-Trace-Id +``` + +## 3. 使用示例 + +告诉我业务代码以后应该怎么写。 + +目标是: + +```java +log.info("开始处理"); +``` + +而不是让业务代码手动管理 TraceId。 + +## 4. 测试结果 + +说明所有测试是否通过。 + +## 5. 风险 + +明确指出: + +- 哪些场景已经支持 +- 哪些场景暂未支持 +- 是否存在 MDC 丢失风险 +- 是否存在线程池上下文污染风险 +- 是否存在 MQ TraceId 丢失风险 + +## 6. 后续演进建议 + +最后说明未来如何从: + +```text +TraceId + MDC +``` + +演进到: + +```text +TraceId ++ +SpanId ++ +ParentSpanId ++ +OpenTelemetry +``` + +但本次不要提前实现完整分布式链路追踪系统。 + +--- + +# 最终验收标准 + +只有同时满足以下条件才算完成: + +```text + traceId=A + │ + ┌───────┴───────┐ + │ │ + Web Task + │ + ▼ + Service + │ + ┌───────┴───────┐ + ▼ ▼ + ThreadPool MQ + │ │ + │ ▼ + │ Consumer + │ │ + └───────┬───────┘ + ▼ + AI/HTTP + │ + ▼ + DB +``` + +同一业务链可以通过: + +```text +traceId=A +``` + +在日志文件、IDEA Console、Kibana 中完整检索。 + +尤其必须保证: + +> **跨线程、跨线程池、跨 MQ、跨 HTTP 的 TraceId 不丢失;任务执行完成后上下文必须清理,不允许线程复用造成 TraceId 污染。** + +请先分析当前代码,再实施修改。 + +不要在没有检查项目现有实现的情况下直接创建重复的 TraceId / MDC / ThreadLocal 体系。 \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java b/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java index f2d1afb2..67f2d431 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java +++ b/fastbee-common/src/main/java/com/fastbee/common/config/DeviceTask.java @@ -1,6 +1,7 @@ package com.fastbee.common.config; import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.trace.MdcTaskDecorator; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -60,7 +61,11 @@ public class DeviceTask { return builder(FastBeeConstant.TASK.FUNCTION_INVOKE_TASK); } - /*内部消费线程*/ + /** + * 内部消费线程 + * 监听循环从消息恢复 TraceId 并逐条清理; + * TaskDecorator 用于让 @Async 消费方法继承监听线程的 TraceId + */ @Bean(FastBeeConstant.TASK.MESSAGE_CONSUME_TASK) public Executor messageConsumeTaskExecutor() { return builder(FastBeeConstant.TASK.MESSAGE_CONSUME_TASK); @@ -104,7 +109,7 @@ public class DeviceTask { } - /*组装线程池*/ + /*组装线程池(带 TaskDecorator,用于跨线程传播 TraceId)*/ private ThreadPoolTaskExecutor builder(String threadNamePrefix) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(coreSize); @@ -115,6 +120,10 @@ public class DeviceTask { executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); //线程池名的前缀 executor.setThreadNamePrefix(threadNamePrefix); + + // 配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + executor.initialize(); return executor; } diff --git a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java index 89be5860..d8eebe75 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java +++ b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java @@ -15,6 +15,8 @@ public interface FastBeeConstant { String MQTT = "mqtt"; String PORT = "port"; String ADAPTER = "adapter"; + /** 链路追踪处理器名 */ + String TRACE = "trace"; String FRAMEDECODER = "frameDecoder"; String DISPATCHER = "dispatcher"; String DECODER = "decoder"; diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java index e45f1ea8..c849844a 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReplyBo.java @@ -8,6 +8,9 @@ import lombok.Data; @Data public class DeviceReplyBo { + /** 链路追踪ID */ + private String traceId; + /*设备下发消息id*/ private String messageId; /*标识符*/ diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java index d4506ccd..edbce754 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceReportBo.java @@ -24,6 +24,9 @@ import java.util.List; @AllArgsConstructor public class DeviceReportBo { + /** 链路追踪ID */ + private String traceId; + /*设备编号或IMEI号*/ private String serialNumber; /*产品ID*/ diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java index 6bcd9adc..68567cf2 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceStatusBo.java @@ -18,6 +18,10 @@ import java.util.Date; @Data @Builder public class DeviceStatusBo { + + /** 链路追踪ID */ + private String traceId; + /** * 设备客户端id */ diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java index b7e3e7ff..cd43f370 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/DeviceTestReportBo.java @@ -11,6 +11,9 @@ import java.util.List; @Data public class DeviceTestReportBo { + /** 链路追踪ID */ + private String traceId; + private Long productId; /** * 设备编号 diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java index acbc8ed5..386c7457 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/InvokeReqDto.java @@ -17,6 +17,9 @@ import java.util.Map; @Data public class InvokeReqDto { + /** 链路追踪ID */ + private String traceId; + @ApiModelProperty(value = "设备编号") private String serialNumber; diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java index f302c489..c5d2f9cc 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/MQSendMessageBo.java @@ -14,6 +14,9 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class MQSendMessageBo { + /** 链路追踪ID */ + private String traceId; + public Boolean isShadow; public Long userId; public Long delay; diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java index fc6db91a..9ddf34ed 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeBo.java @@ -17,6 +17,9 @@ import java.math.BigDecimal; @JsonInclude(JsonInclude.Include.NON_EMPTY) public class OtaUpgradeBo { + /** 链路追踪ID */ + private String traceId; + // 设备编码 private String serialNumber; // 升级任务ID diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java index ae5e3d50..96585439 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/mq/ota/OtaUpgradeDelayTask.java @@ -17,6 +17,11 @@ import java.util.concurrent.TimeUnit; @Data public class OtaUpgradeDelayTask implements Delayed { + /** + * 链路追踪ID + */ + private String traceId; + /** * 固件id */ diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java b/fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java new file mode 100644 index 00000000..26758f30 --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/MdcTaskDecorator.java @@ -0,0 +1,75 @@ +package com.fastbee.common.trace; + +import com.fastbee.common.trace.TraceContext.TraceSnapshot; +import org.springframework.core.task.TaskDecorator; + +import java.util.concurrent.Callable; + +/** + * 线程池 TraceId 传播装饰器 + * 解决 @Async 异步任务 TraceId 丢失问题 + * + *

使用方法: + *

+ * ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ * executor.setTaskDecorator(new MdcTaskDecorator());
+ * 
+ * + *

工作原理: + *

    + *
  1. 在任务提交时,捕获当前线程的 TraceContext 快照
  2. + *
  3. 在任务执行时,恢复快照到新线程(MDC 由 TraceContext 统一同步)
  4. + *
  5. 在任务完成后,清理上下文防止线程池污染
  6. + *
+ * + *

说明:MDC 的写入/清理全部由 {@link TraceContext} 统一管理, + * 本装饰器只负责快照与恢复,避免 MDC 与 TraceContext 双重操作导致的不一致。 + * + * @author FastBee + */ +public class MdcTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + // 捕获当前线程的 TraceContext 快照(跨线程传播的唯一依据) + TraceSnapshot snapshot = TraceContext.snapshot(); + + return () -> { + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + // 父线程无上下文,生成新的 TraceId + TraceContext.getOrCreateTraceId(); + } + try { + runnable.run(); + } finally { + // 清理上下文,防止线程池复用造成污染 + TraceContext.clear(); + } + }; + } + + /** + * 装饰 Callable + * + * @param task Callable 任务 + * @return 装饰后的 Callable + */ + public Callable decorate(Callable task) { + TraceSnapshot snapshot = TraceContext.snapshot(); + + return () -> { + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + TraceContext.getOrCreateTraceId(); + } + try { + return task.call(); + } finally { + TraceContext.clear(); + } + }; + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java new file mode 100644 index 00000000..7c807628 --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceContext.java @@ -0,0 +1,270 @@ +package com.fastbee.common.trace; + +import lombok.Data; +import org.slf4j.MDC; + +import java.io.Serializable; + +/** + * 统一追踪上下文 + * 管理 traceId, tenantId, userId 等追踪信息 + * + *

核心特性: + *

    + *
  • 基于 ThreadLocal 实现线程隔离
  • + *
  • 自动同步到 MDC 用于日志输出
  • + *
  • 支持 snapshot/restore 用于跨线程传播
  • + *
+ * + *

使用示例: + *

+ * // Web请求入口
+ * TraceContext.setTraceId(TraceIdGenerator.generate());
+ *
+ * // 异步任务(通过 TaskDecorator 自动传播)
+ * @Async
+ * public void asyncTask() {
+ *     log.info("处理中"); // 日志自动包含 traceId
+ * }
+ *
+ * // 手动传播(用于非 Spring 管理的线程池)
+ * TraceSnapshot snapshot = TraceContext.snapshot();
+ * executor.submit(() -> {
+ *     TraceContext.restore(snapshot);
+ *     try {
+ *         // 业务逻辑
+ *     } finally {
+ *         TraceContext.clear();
+ *     }
+ * });
+ * 
+ * + * @author FastBee + */ +public class TraceContext { + + /** + * TraceId 在 MDC 中的 key + */ + public static final String TRACE_ID_KEY = "traceId"; + + /** + * TenantId 在 MDC 中的 key + * 预留字段,暂未使用 + */ + public static final String TENANT_ID_KEY = "tenantId"; + + /** + * UserId 在 MDC 中的 key + * 预留字段,暂未使用 + */ + public static final String USER_ID_KEY = "userId"; + + /** + * 线程本地上下文存储 + */ + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + /** + * 追踪上下文快照 + * 用于跨线程传递 + */ + @Data + public static class TraceSnapshot implements Serializable { + private static final long serialVersionUID = 1L; + + private String traceId; + private Long tenantId; // 预留字段 + private Long userId; // 预留字段 + + /** + * 将快照应用到 MDC + * 先移除旧值再写入,保证 MDC 状态与快照精确一致(包括 null 字段) + */ + public void applyToMdc() { + MDC.remove(TRACE_ID_KEY); + MDC.remove(TENANT_ID_KEY); + MDC.remove(USER_ID_KEY); + if (traceId != null) { + MDC.put(TRACE_ID_KEY, traceId); + } + if (tenantId != null) { + MDC.put(TENANT_ID_KEY, String.valueOf(tenantId)); + } + if (userId != null) { + MDC.put(USER_ID_KEY, String.valueOf(userId)); + } + } + } + + // ================== Get Methods ================== + + /** + * 获取当前 TraceId + * + * @return TraceId,如果不存在返回 null + */ + public static String getTraceId() { + TraceSnapshot snapshot = CONTEXT.get(); + return snapshot != null ? snapshot.getTraceId() : null; + } + + /** + * 获取当前 TenantId(预留) + * + * @return TenantId,如果不存在返回 null + */ + public static Long getTenantId() { + TraceSnapshot snapshot = CONTEXT.get(); + return snapshot != null ? snapshot.getTenantId() : null; + } + + /** + * 获取当前 UserId(预留) + * + * @return UserId,如果不存在返回 null + */ + public static Long getUserId() { + TraceSnapshot snapshot = CONTEXT.get(); + return snapshot != null ? snapshot.getUserId() : null; + } + + // ================== Set Methods ================== + + /** + * 设置 TraceId + * 同时更新 ThreadLocal 和 MDC + * + * @param traceId TraceId + */ + public static void setTraceId(String traceId) { + TraceSnapshot snapshot = CONTEXT.get(); + if (snapshot == null) { + snapshot = new TraceSnapshot(); + CONTEXT.set(snapshot); + } + snapshot.setTraceId(traceId); + if (traceId != null) { + MDC.put(TRACE_ID_KEY, traceId); + } else { + MDC.remove(TRACE_ID_KEY); + } + } + + /** + * 设置 TenantId(预留) + * 同时更新 ThreadLocal 和 MDC + * + * @param tenantId TenantId + */ + public static void setTenantId(Long tenantId) { + TraceSnapshot snapshot = CONTEXT.get(); + if (snapshot == null) { + snapshot = new TraceSnapshot(); + CONTEXT.set(snapshot); + } + snapshot.setTenantId(tenantId); + if (tenantId != null) { + MDC.put(TENANT_ID_KEY, String.valueOf(tenantId)); + } else { + MDC.remove(TENANT_ID_KEY); + } + } + + /** + * 设置 UserId(预留) + * 同时更新 ThreadLocal 和 MDC + * + * @param userId UserId + */ + public static void setUserId(Long userId) { + TraceSnapshot snapshot = CONTEXT.get(); + if (snapshot == null) { + snapshot = new TraceSnapshot(); + CONTEXT.set(snapshot); + } + snapshot.setUserId(userId); + if (userId != null) { + MDC.put(USER_ID_KEY, String.valueOf(userId)); + } else { + MDC.remove(USER_ID_KEY); + } + } + + // ================== Convenience Methods ================== + + /** + * 获取或创建 TraceId + * 如果当前没有 TraceId,自动生成一个新的 + * + * @return TraceId + */ + public static String getOrCreateTraceId() { + String traceId = getTraceId(); + if (traceId == null || traceId.isEmpty()) { + traceId = TraceIdGenerator.generate(); + setTraceId(traceId); + } + return traceId; + } + + /** + * 确保当前有 TraceId + * 如果传入的 traceId 不为空,使用传入的;否则生成新的 + * + * @param traceId 可选的 TraceId + * @return 最终的 TraceId + */ + public static String ensureTraceId(String traceId) { + if (traceId != null && !traceId.isEmpty()) { + setTraceId(traceId); + return traceId; + } + return getOrCreateTraceId(); + } + + // ================== Snapshot & Restore ================== + + /** + * 创建当前上下文的快照 + * 用于跨线程传递 + * + * @return TraceSnapshot 快照,如果当前无上下文返回 null + */ + public static TraceSnapshot snapshot() { + TraceSnapshot current = CONTEXT.get(); + if (current == null) { + return null; + } + TraceSnapshot copy = new TraceSnapshot(); + copy.setTraceId(current.getTraceId()); + copy.setTenantId(current.getTenantId()); + copy.setUserId(current.getUserId()); + return copy; + } + + /** + * 从快照恢复上下文 + * + * @param snapshot 快照,如果为 null 则清理上下文 + */ + public static void restore(TraceSnapshot snapshot) { + if (snapshot == null) { + clear(); + return; + } + CONTEXT.set(snapshot); + snapshot.applyToMdc(); + } + + /** + * 清除当前上下文 + * 同时清除 ThreadLocal 和 MDC + */ + public static void clear() { + CONTEXT.remove(); + MDC.remove(TRACE_ID_KEY); + MDC.remove(TENANT_ID_KEY); + MDC.remove(USER_ID_KEY); + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java new file mode 100644 index 00000000..755780bb --- /dev/null +++ b/fastbee-common/src/main/java/com/fastbee/common/trace/TraceIdGenerator.java @@ -0,0 +1,106 @@ +package com.fastbee.common.trace; + +import org.slf4j.MDC; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; + +/** + * TraceId 生成器 + * 生成纯数字格式的 TraceId + * + *

格式:基于时间戳 + 序列号 + 随机数 + *

示例:752431931275776594 + * + *

特点: + *

    + *
  • 纯数字格式,便于存储和索引
  • + *
  • 时间有序,大致按生成时间排序
  • + *
  • 线程安全,使用 synchronized 保证
  • + *
  • 高并发友好,单机每秒可生成百万级 ID
  • + *
+ * + * @author FastBee + */ +public class TraceIdGenerator { + + /** + * 起始时间戳 (2023-01-01 00:00:00) + * 用于减少时间戳位数,延长 ID 可用年限 + */ + private static final long START_TIMESTAMP = 1672531200000L; + + /** + * 序列号掩码:16 bit(0 ~ 65535) + */ + private static final long SEQUENCE_MASK = 0xFFFFL; + + /** + * 序列号,同一毫秒内递增 + */ + private static final AtomicLong sequence = new AtomicLong(0); + + /** + * 上次生成时间戳 + */ + private static volatile long lastTimestamp = -1L; + + /** + * 生成纯数字格式的 TraceId + * + * @return 纯数字 TraceId 字符串 + */ + public static String generate() { + return String.valueOf(generateLong()); + } + + /** + * 生成 long 类型的 TraceId + * 使用 synchronized 保证线程安全 + * + *

位域分配(低到高): + *

    + *
  • 随机数 6 bit(第 0~5 位)
  • + *
  • 序列号 16 bit(第 6~21 位)
  • + *
  • 时间差 剩余位(第 22 位及以上)
  • + *
+ * + * @return long 类型 TraceId + */ + public static synchronized long generateLong() { + long timestamp = System.currentTimeMillis(); + + // 时钟回拨保护:时间不允许倒退,沿用上次时间戳 + if (timestamp < lastTimestamp) { + timestamp = lastTimestamp; + } + + if (timestamp == lastTimestamp) { + // 同毫秒:序列号递增并回写(& MASK 防止溢出侵入时间位域) + sequence.set((sequence.get() + 1) & SEQUENCE_MASK); + if (sequence.get() == 0) { + // 序列号用尽,等待下一毫秒 + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence.set(0); + } + lastTimestamp = timestamp; + + // 时间差(高位) | 序列号(中位) | 随机数(低位 6 bit) + long timeDiff = timestamp - START_TIMESTAMP; + long random = ThreadLocalRandom.current().nextInt(64); + return (timeDiff << 22) | (sequence.get() << 6) | random; + } + + /** + * 等待下一毫秒 + */ + private static long tilNextMillis(long lastTimestamp) { + long timestamp = System.currentTimeMillis(); + while (timestamp <= lastTimestamp) { + timestamp = System.currentTimeMillis(); + } + return timestamp; + } +} \ No newline at end of file diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java index fdd7138c..558a4223 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/TraceIdUtils.java @@ -1,188 +1,64 @@ package com.fastbee.common.utils; -import com.fastbee.common.utils.uuid.IdUtils; -import org.slf4j.MDC; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceContext.TraceSnapshot; -import java.util.Map; import java.util.concurrent.Callable; -import java.util.concurrent.ThreadLocalRandom; /** * 日志追踪ID工具类 - * 统一生成TraceId的逻辑,使用IdWorker和ThreadLocalRandom + * 提供向后兼容的 API,内部委托给 TraceContext * * @author Sumuxi + * @deprecated 建议直接使用 {@link TraceContext} */ public class TraceIdUtils { /** - * TraceId在MDC中的key - */ - public static final String TRACE_ID_KEY = "traceId"; - - /** - * IdWorker实例(单例模式) - */ - private static volatile IdUtils idWorker; - - /** - * 获取IdWorker实例(懒加载单例) + * 包装 Runnable:透传 TraceContext(MDC 由 TraceContext 统一同步) + * 用于非 Spring 管理的线程池(如 ScheduledExecutorService) * - * @return IdWorker实例 - */ - private static IdUtils getIdWorker() { - if (idWorker == null) { - synchronized (TraceIdUtils.class) { - if (idWorker == null) { - // 使用机器ID,可以根据实际情况配置 - // 这里使用1作为默认workerId,可以通过配置文件读取 - // IdUtils构造函数:workerId, datacenterId,-1表示随机生成 - idWorker = new IdUtils(-1L, -1L); - } - } - } - return idWorker; - } - - /** - * 生成TraceId - * 格式:IdWorker.nextId() * 1000 + ThreadLocalRandom.nextInt(1000) - * 参考:generateClueId() 的实现方式 - * 使用Math.abs确保TraceId为正数 - * - * @return TraceId - */ - public static long generateTraceId() { - long id = getIdWorker().nextId(); - // 使用绝对值确保TraceId为正数,避免负数显示 - // 注意:Math.abs(id) * 1000 可能会溢出,所以需要对最终结果再次使用Math.abs() - long traceId = Math.abs(id) * 1000 + ThreadLocalRandom.current().nextInt(1000); - // 确保最终结果为非负数(处理溢出情况) - traceId = Math.abs(traceId); - MDC.put(TRACE_ID_KEY, String.valueOf(traceId)); - return traceId; - } - - /** - * 生成TraceId并放入MDC - * 返回字符串格式的TraceId - * - * @return TraceId字符串 - */ - public static String generateTraceIdString() { - long traceId = generateTraceId(); - return String.valueOf(traceId); - } - - /** - * 设置TraceId到MDC - * - * @param traceId TraceId - */ - public static void setTraceId(String traceId) { - if (traceId != null && !traceId.isEmpty()) { - MDC.put(TRACE_ID_KEY, traceId); - } - } - - /** - * 设置TraceId到MDC(long类型) - * - * @param traceId TraceId - */ - public static void setTraceId(long traceId) { - MDC.put(TRACE_ID_KEY, String.valueOf(traceId)); - } - - /** - * 从MDC获取TraceId - * - * @return TraceId - */ - public static String getTraceId() { - return MDC.get(TRACE_ID_KEY); - } - - /** - * 确保当前线程 MDC 中存在 TraceId。 - * - 已存在:直接返回 - * - 不存在:生成并写入 MDC - */ - public static String ensureTraceId() { - String traceId = getTraceId(); - if (traceId == null || traceId.isEmpty()) { - traceId = generateTraceIdString(); - } - return traceId; - } - - /** - * 使用指定 traceId 覆盖当前线程 MDC(为空则生成)。 - * 主要用于跨线程/跨进程透传后的落地。 - */ - public static String ensureTraceId(String traceId) { - if (traceId == null || traceId.isEmpty()) { - return ensureTraceId(); - } - setTraceId(traceId); - return traceId; - } - - /** - * 包装 Runnable:透传提交线程的 MDC,上下文中无 traceId 则补齐;执行完自动清理/还原。 + * @param task Runnable 任务 + * @return 包装后的 Runnable */ public static Runnable wrapWithMdc(Runnable task) { - final Map captured = MDC.getCopyOfContextMap(); + final TraceSnapshot snapshot = TraceContext.snapshot(); + return () -> { - Map previous = MDC.getCopyOfContextMap(); + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + TraceContext.getOrCreateTraceId(); + } try { - if (captured == null || captured.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(captured); - } - ensureTraceId(); task.run(); } finally { - if (previous == null || previous.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(previous); - } + TraceContext.clear(); } }; } /** - * 包装 Callable:透传提交线程的 MDC,上下文中无 traceId 则补齐;执行完自动清理/还原。 + * 包装 Callable:透传 TraceContext(MDC 由 TraceContext 统一同步) + * 用于非 Spring 管理的线程池(如 ScheduledExecutorService) + * + * @param task Callable 任务 + * @return 包装后的 Callable */ public static Callable wrapWithMdc(Callable task) { - final Map captured = MDC.getCopyOfContextMap(); + final TraceSnapshot snapshot = TraceContext.snapshot(); + return () -> { - Map previous = MDC.getCopyOfContextMap(); + if (snapshot != null) { + TraceContext.restore(snapshot); + } else { + TraceContext.getOrCreateTraceId(); + } try { - if (captured == null || captured.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(captured); - } - ensureTraceId(); return task.call(); } finally { - if (previous == null || previous.isEmpty()) { - MDC.clear(); - } else { - MDC.setContextMap(previous); - } + TraceContext.clear(); } }; } - - /** - * 清除MDC中的TraceId - */ - public static void clearTraceId() { - MDC.remove(TRACE_ID_KEY); - } -} - +} \ No newline at end of file diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java b/fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java new file mode 100644 index 00000000..e4691c91 --- /dev/null +++ b/fastbee-framework/src/main/java/com/fastbee/framework/aspectj/ScheduledTraceAspect.java @@ -0,0 +1,47 @@ +package com.fastbee.framework.aspectj; + +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * @Scheduled 定时任务 TraceId 切面 + * 自动为所有 Spring 定时任务方法生成 TraceId + * + * @author FastBee + */ +@Slf4j +@Aspect +@Component +@Order(1) // 高优先级,确保最先执行 +public class ScheduledTraceAspect { + + /** + * 拦截所有 @Scheduled 方法 + */ + @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)") + public Object aroundScheduled(ProceedingJoinPoint joinPoint) throws Throwable { + String methodName = joinPoint.getSignature().toShortString(); + + // 生成 TraceId + String traceId = TraceIdGenerator.generate(); + TraceContext.setTraceId(traceId); + + log.debug("定时任务开始执行: {}", methodName); + + try { + return joinPoint.proceed(); + } catch (Throwable e) { + log.error("定时任务执行异常: {}", methodName, e); + throw e; + } finally { + log.debug("定时任务执行完成: {}", methodName); + TraceContext.clear(); + } + } +} \ No newline at end of file diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java b/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java index d460de10..6a4859e5 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/config/OtaThreadPoolConfig.java @@ -1,6 +1,7 @@ package com.fastbee.framework.config; import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.trace.MdcTaskDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -30,6 +31,10 @@ public class OtaThreadPoolConfig { executor.setThreadNamePrefix(threadNamePrefix); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 新增:配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + // 进行加载 executor.initialize(); return executor; diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java b/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java index cf18f388..6cbeeb7b 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/config/ThreadPoolConfig.java @@ -1,5 +1,6 @@ package com.fastbee.framework.config; +import com.fastbee.common.trace.MdcTaskDecorator; import com.fastbee.common.utils.Threads; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.springframework.context.annotation.Bean; @@ -38,6 +39,10 @@ public class ThreadPoolConfig { executor.setKeepAliveSeconds(keepAliveSeconds); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 新增:配置 TaskDecorator,自动传播 TraceId + executor.setTaskDecorator(new MdcTaskDecorator()); + return executor; } diff --git a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java index b892f4ea..af9675c8 100644 --- a/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java +++ b/fastbee-framework/src/main/java/com/fastbee/framework/interceptor/TraceIdInterceptor.java @@ -1,9 +1,9 @@ package com.fastbee.framework.interceptor; -import com.fastbee.common.utils.TraceIdUtils; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -16,24 +16,26 @@ import javax.servlet.http.HttpServletResponse; @Component public class TraceIdInterceptor implements HandlerInterceptor { + private static final String TRACE_ID_HEADER = "X-Trace-Id"; + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求URI String requestURI = request.getRequestURI(); // 尝试从请求头中获取TraceId(支持分布式追踪) - String traceId = request.getHeader("X-Trace-Id"); + String traceId = request.getHeader(TRACE_ID_HEADER); // 如果请求头中没有TraceId,则生成一个新的 if (traceId == null || traceId.isEmpty()) { - traceId = TraceIdUtils.generateTraceIdString(); - } else { - // 如果请求头中有TraceId,设置到MDC - TraceIdUtils.setTraceId(traceId); + traceId = TraceIdGenerator.generate(); } + // 设置到 TraceContext(自动同步到 MDC) + TraceContext.setTraceId(traceId); + // 将TraceId放入响应头,方便前端获取 - response.setHeader("X-Trace-Id", traceId); + response.setHeader(TRACE_ID_HEADER, traceId); // 打印请求路径日志 org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TraceIdInterceptor.class); @@ -44,8 +46,8 @@ public class TraceIdInterceptor implements HandlerInterceptor { @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - // 请求完成后清除MDC中的TraceId,避免内存泄漏 - TraceIdUtils.clearTraceId(); + // 请求完成后清除 TraceContext,避免内存泄漏 + TraceContext.clear(); } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java index c24fd805..356168fb 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java @@ -2,9 +2,10 @@ package com.fastbee.iot.data.job; import com.fastbee.common.constant.Constants; import com.fastbee.common.constant.ScheduleConstants; +import com.fastbee.common.trace.TraceContext; +import com.fastbee.common.trace.TraceIdGenerator; import com.fastbee.common.utils.ExceptionUtil; import com.fastbee.common.utils.StringUtils; -import com.fastbee.common.utils.TraceIdUtils; import com.fastbee.common.utils.bean.BeanUtils; import com.fastbee.common.utils.spring.SpringUtils; import com.fastbee.iot.domain.DeviceJob; @@ -56,8 +57,8 @@ public abstract class DeviceAbstractQuartzJob implements Job { protected void before(JobExecutionContext context, DeviceJob deviceJob) { threadLocal.set(new Date()); - // 为设备定时任务生成TraceId,使用统一的生成逻辑 - TraceIdUtils.generateTraceId(); + // 为设备定时任务生成 TraceId + TraceContext.setTraceId(TraceIdGenerator.generate()); log.info("设备定时任务开始执行: {} - {}", deviceJob.getJobName(), deviceJob.getJobGroup()); } @@ -106,10 +107,10 @@ public abstract class DeviceAbstractQuartzJob implements Job { // 写入数据库当中 SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); - // 清除TraceId,避免内存泄漏 - TraceIdUtils.clearTraceId(); - log.info("设备定时任务执行完成: {} - {}, 耗时: {}ms", deviceJob.getJobName(), deviceJob.getJobGroup(), runMs); + + // 清除 TraceContext + TraceContext.clear(); } /** diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java index 0b84e340..8679a1af 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceOtherListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceOtherMsgConsumer; import com.fastbee.mq.queue.DeviceOtherQueue; import lombok.extern.slf4j.Slf4j; @@ -26,7 +27,15 @@ public class DeviceOtherListen { while (true) { try { DeviceReportBo reportBo = DeviceOtherQueue.take(); - otherMsgConsumer.consume(reportBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(reportBo.getTraceId()); + + try { + otherMsgConsumer.consume(reportBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>emq数据转发异常"); } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java index 167a9b41..06388c3c 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DevicePropFetchListen.java @@ -1,6 +1,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DevicePropFetchConsumer; import com.fastbee.iot.model.modbus.ModbusPollJob; import com.fastbee.mq.queue.DevicePropFetchQueue; @@ -27,9 +28,20 @@ public class DevicePropFetchListen { while (true) { try { ModbusPollJob take = DevicePropFetchQueue.take(); - devicePropFetchConsumer.consume(take); + + // 恢复 TraceId + TraceContext.ensureTraceId(take.getTraceId()); + + try { + devicePropFetchConsumer.consume(take); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>设备属性获取异常", e); + } finally { + // 确保任何情况下都清理 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java index fee5854f..8f3cf966 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceReplyMsgConsumer; import com.fastbee.mq.queue.DeviceReplyQueue; import lombok.extern.slf4j.Slf4j; @@ -27,8 +28,16 @@ public class DeviceReplyListen { try { /*读队列消息*/ DeviceReportBo reportBo = DeviceReplyQueue.take(); - /*处理消息*/ - deviceReplyMsgHandler.consume(reportBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(reportBo.getTraceId()); + + try { + /*处理消息*/ + deviceReplyMsgHandler.consume(reportBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>设备回调消息监听异常", e); } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java index caad7495..fa28bb03 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReportListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceReportMsgConsumer; import com.fastbee.mq.queue.DeviceReportQueue; import lombok.extern.slf4j.Slf4j; @@ -27,10 +28,21 @@ public class DeviceReportListen { try { /*取出数据*/ DeviceReportBo reportBo = DeviceReportQueue.take(); - /*处理数据*/ - reportMsgConsumer.consume(reportBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(reportBo.getTraceId()); + + try { + /*处理数据*/ + reportMsgConsumer.consume(reportBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>设备上报数据监听异常", e); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java index 6252c0cb..07bdf6b9 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceStatusListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceStatusBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceStatusConsumer; import com.fastbee.mq.queue.DeviceStatusQueue; import lombok.extern.slf4j.Slf4j; @@ -26,10 +27,20 @@ public class DeviceStatusListen { try { while (true) { DeviceStatusBo status = DeviceStatusQueue.take(); - deviceStatusConsumer.consume(status); + + // 恢复 TraceId + TraceContext.ensureTraceId(status.getTraceId()); + + try { + deviceStatusConsumer.consume(status); + } finally { + TraceContext.clear(); + } } } catch (Exception e) { log.error("设备状态监听错误", e); + } finally { + TraceContext.clear(); } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java index 1b4de8a7..072351c0 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.DeviceTestReportBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.DeviceTestConsumer; import com.fastbee.mq.queue.DeviceTestQueue; import lombok.extern.slf4j.Slf4j; @@ -26,7 +27,14 @@ public class DeviceTestListen { try { DeviceTestReportBo take = DeviceTestQueue.take(); - deviceTestConsumer.consume(take); + // 恢复 TraceId + TraceContext.ensureTraceId(take.getTraceId()); + + try { + deviceTestConsumer.consume(take); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>emq数据转发异常"); } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java index a631bd22..8f24f30e 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.MQSendMessageBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.consumer.FunctionInvokeConsumer; import com.fastbee.mq.queue.FunctionInvokeQueue; import lombok.extern.slf4j.Slf4j; @@ -26,7 +27,15 @@ public class FunctionInvokeListen { while (true) { try { MQSendMessageBo sendBo = FunctionInvokeQueue.take(); - functionInvokeConsumer.handler(sendBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(sendBo.getTraceId()); + + try { + functionInvokeConsumer.handler(sendBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("=>下发服务消费异常", e); } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java index 56a6ded2..94a7d13c 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpGradeListener.java @@ -4,6 +4,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.domain.entity.SysMenu; import com.fastbee.common.core.mq.ota.OtaUpgradeDelayTask; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.StringUtils; import com.fastbee.iot.data.service.IOtaUpgradeService; import com.fastbee.mq.queue.DelayUpgradeQueue; @@ -48,12 +49,22 @@ public class UpGradeListener implements ApplicationRunner { OtaUpgradeDelayTask task = DelayUpgradeQueue.task(); if (StringUtils.isNotNull(task)) { Date startTime = task.getStartTime(); - otaUpgradeService.upgrade(task); - log.info("=>开始OTA升级时间{}", startTime); + + // 恢复 TraceId + TraceContext.ensureTraceId(task.getTraceId()); + + try { + otaUpgradeService.upgrade(task); + log.info("=>开始OTA升级时间{}", startTime); + } finally { + TraceContext.clear(); + } } continue; } catch (Exception e) { log.error("=>OTA升级监听异常", e); + } finally { + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java index e7ea9a22..eb59af71 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/UpgradeListen.java @@ -2,6 +2,7 @@ package com.fastbee.iot.data.listen; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.core.mq.ota.OtaUpgradeBo; +import com.fastbee.common.trace.TraceContext; import com.fastbee.iot.data.service.IMqttMessagePublish; import com.fastbee.mq.queue.OtaUpgradeQueue; import lombok.extern.slf4j.Slf4j; @@ -28,10 +29,20 @@ public class UpgradeListen { try { /*获取队列中的OTA升级消息*/ OtaUpgradeBo upgradeBo = OtaUpgradeQueue.take(); - // OTA升级处理 - functionSendService.upGradeOTA(upgradeBo); + + // 恢复 TraceId + TraceContext.ensureTraceId(upgradeBo.getTraceId()); + + try { + // OTA升级处理 + functionSendService.upGradeOTA(upgradeBo); + } finally { + TraceContext.clear(); + } } catch (Exception e) { log.error("->OTA消息监听异常", e); + } finally { + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java index abe92afd..e522d24a 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/FirmwareTaskServiceImpl.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fastbee.common.core.mq.ota.OtaUpgradeDelayTask; import com.fastbee.common.enums.OTAUpgrade; import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.MessageUtils; import com.fastbee.common.utils.StringUtils; @@ -140,7 +141,9 @@ public class FirmwareTaskServiceImpl extends ServiceImpl放置在协议 pipeline 的最前端(编解码器之前),为每条入站报文生成独立的 TraceId, + * 使解码、分发、业务处理全链路的日志都携带同一个 TraceId。 + * + *

注意:Netty EventLoop 线程会被多连接/多报文复用, + * 因此必须在处理结束后清理,否则会造成 TraceId 串线。 + * + * @author FastBee + */ +@ChannelHandler.Sharable +public class TraceHandler extends ChannelInboundHandlerAdapter { + + /** + * 共享实例(无状态,可跨 Channel 复用) + */ + public static final TraceHandler INSTANCE = new TraceHandler(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 每条设备报文都是独立业务事件,生成新的 TraceId + TraceContext.setTraceId(TraceIdGenerator.generate()); + try { + ctx.fireChannelRead(msg); + } finally { + TraceContext.clear(); + } + } +} \ No newline at end of file diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java index 6a8ba7bb..d5ddb77a 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/TCPServer.java @@ -1,5 +1,6 @@ package com.fastbee.server; +import com.fastbee.base.core.hanler.TraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.server.config.NettyConfig; import com.fastbee.server.handler.*; @@ -61,9 +62,11 @@ public class TCPServer extends Server { @Override protected void initChannel(NioSocketChannel channel) throws Exception { channel.pipeline() + // 第一步、链路追踪(必须位于编解码器之前,让解码日志也带 TraceId) + .addLast(FastBeeConstant.SERVER.TRACE, TraceHandler.INSTANCE) // 设置心跳时间 .addLast(new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime)) - // 第一步、添加粘包处理器 + // 第二步、添加粘包处理器 .addLast(FastBeeConstant.SERVER.FRAMEDECODER, new ProtocolSniffingDecoder(config.maxFrameLength)) // 第二步、添加消息适配器 .addLast(FastBeeConstant.SERVER.ADAPTER, adapter) diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java index 35ffb17f..ae05b9dd 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/UDPServer.java @@ -1,5 +1,6 @@ package com.fastbee.server; +import com.fastbee.base.core.hanler.TraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.server.config.NettyConfig; import com.fastbee.server.handler.DispatcherHandler; @@ -52,6 +53,8 @@ public class UDPServer extends Server { @Override protected void initChannel(NioDatagramChannel channel) throws Exception { channel.pipeline() + // 链路追踪(位于编解码器之前,让解码日志也带 TraceId) + .addLast(FastBeeConstant.SERVER.TRACE, TraceHandler.INSTANCE) .addLast(FastBeeConstant.SERVER.ADAPTER, adapter) .addLast(FastBeeConstant.SERVER.DECODER, decoder) .addLast(FastBeeConstant.SERVER.ENCODER, encoder) diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java index 8b8abca0..008e7564 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DispatcherHandler.java @@ -13,6 +13,7 @@ import com.fastbee.common.core.mq.DeviceTestReportBo; import com.fastbee.common.core.protocol.Message; import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; import com.fastbee.common.enums.ServerType; +import com.fastbee.common.trace.TraceContext; import com.fastbee.common.utils.DateUtils; import com.fastbee.mq.producer.MessageProducer; import io.netty.channel.ChannelHandler; @@ -61,6 +62,7 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter { s.increment(); } + /*TraceId 由 pipeline 头部的 TraceHandler 统一生成与清理*/ Packet packet = (Packet) msg; Message request = packet.message; // 判断是否注册 @@ -127,7 +129,16 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter { } } else { if (handler.async) { - executor.execute(() -> channelRead0(ctx, packet, handler)); + /*异步执行:手动透传 TraceId(业务线程池非Spring管理,无 TaskDecorator)*/ + final String traceId = TraceContext.getTraceId(); + executor.execute(() -> { + TraceContext.ensureTraceId(traceId); + try { + channelRead0(ctx, packet, handler); + } finally { + TraceContext.clear(); + } + }); } else { channelRead0(ctx, packet, handler); } diff --git a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java index b6d5d146..6708dc56 100644 --- a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java +++ b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/adapter/MqttMessageDelegate.java @@ -55,6 +55,8 @@ public class MqttMessageDelegate { log.error("=>客户端未连接"); throw new ServiceException("客户端未连接"); } + + /*TraceId 由 pipeline 头部的 TraceHandler 统一生成与清理*/ Optional.of(processor.get(messageType)) .ifPresent(mqttHandler -> mqttHandler.handler(ctx, message)); } diff --git a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java index f81f9342..7fe40f53 100644 --- a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java +++ b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/MqttServer.java @@ -1,5 +1,6 @@ package com.fastbee.mqtt.server; +import com.fastbee.base.core.hanler.TraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.mqtt.handler.adapter.MqttMessageAdapter; import com.fastbee.server.Server; @@ -54,6 +55,8 @@ public class MqttServer extends Server { channel.pipeline() .addFirst(FastBeeConstant.SERVER.IDLE , new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime, TimeUnit.SECONDS)) + // 链路追踪(位于编解码器之前,让解码日志也带 TraceId) + .addFirst(FastBeeConstant.SERVER.TRACE, TraceHandler.INSTANCE) .addLast(FastBeeConstant.SERVER.DECODER, new MqttDecoder(1024 * 1024 * 2)) .addLast(FastBeeConstant.SERVER.ENCODER, MqttEncoder.INSTANCE) .addLast(messageAdapter); diff --git a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java index 33ea603d..a7962116 100644 --- a/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java +++ b/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/server/WebSocketServer.java @@ -1,5 +1,6 @@ package com.fastbee.mqtt.server; +import com.fastbee.base.core.hanler.TraceHandler; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.mqtt.codec.WebSocketMqttCodec; import com.fastbee.mqtt.handler.adapter.MqttMessageAdapter; @@ -49,6 +50,8 @@ public class WebSocketServer extends Server { ch.pipeline() .addFirst(FastBeeConstant.WS.HEART_BEAT , new IdleStateHandler(0, 0, 70)) + /*链路追踪(位于编解码器之前,让解码日志也带 TraceId)*/ + .addFirst(FastBeeConstant.SERVER.TRACE, TraceHandler.INSTANCE) /*http请求响应*/ .addLast(FastBeeConstant.WS.HTTP_SERVER_CODEC, new HttpServerCodec()) /*聚合header与body组成完整的Http请求,最大数据量为1Mb*/ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java index 237d444e..0c34e4f5 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/modbus/ModbusPollJob.java @@ -11,6 +11,9 @@ import lombok.Data; @Data public class ModbusPollJob { + /** 链路追踪ID */ + private String traceId; + private ModbusPollMsg pollMsg; private DeviceJob deviceJob; diff --git a/sql/log_clean_task_init.sql b/sql/log_clean_task_init.sql index 36e0efb2..f12ccdcf 100644 --- a/sql/log_clean_task_init.sql +++ b/sql/log_clean_task_init.sql @@ -19,7 +19,7 @@ INSERT INTO `sys_job` ( ) VALUES ( '日志清理任务', -- 任务名称 'SYSTEM', -- 任务组名 - 'logCleanTask.cleanLogs()', -- 调用目标字符串 + 'logCleanTask.cleanLogs(30)', -- 调用目标字符串 '0 0 0 * * ?', -- Cron 表达式:每天凌晨 00:00:00 '3', -- 错误策略:不触发立即执行 '1', -- 禁止并发执行 From f30ffda8894134b9ef5ec336629f708b32d29ab1 Mon Sep 17 00:00:00 2001 From: zhugongping Date: Wed, 16 Sep 2026 15:07:16 +0800 Subject: [PATCH 13/19] =?UTF-8?q?20260915:traceId=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/fastbee/common/utils/StringUtils.java | 8 ++-- .../consumer/DevicePropFetchConsumer.java | 2 +- .../iot/data/job/DeviceAbstractQuartzJob.java | 15 ++++---- .../DeviceDisallowConcurrentExecution.java | 1 + .../iot/data/job/DeviceJobExecution.java | 1 + .../fastbee/iot/data/job/DeviceJobInvoke.java | 38 +++++++++---------- .../iot/data/job/SyncDeviceStatusJob.java | 2 +- .../iot/data/listen/DeviceReplyListen.java | 3 ++ .../iot/data/listen/DeviceTestListen.java | 3 ++ .../iot/data/listen/FunctionInvokeListen.java | 3 ++ .../service/impl/DeviceJobServiceImpl.java | 4 ++ .../impl/DeviceMessageServiceImpl.java | 8 ++-- .../quartz/controller/SysJobController.java | 8 ++-- .../quartz/util/AbstractQuartzJob.java | 16 ++++---- .../fastbee/quartz/util/JobInvokeUtil.java | 6 +-- 15 files changed, 64 insertions(+), 54 deletions(-) diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java index 3b5b0e7b..ce9e00d4 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/StringUtils.java @@ -295,21 +295,19 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { /** * 判断给定的set列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value * - * @param set 给定的集合 + * @param collection 给定的集合 * @param array 给定的数组 * @return boolean 结果 */ public static boolean containsAny(Collection collection, String... array) { - if (isEmpty(collection) || isEmpty(array)) { - return false; - } else { + if (!isEmpty(collection) && !isEmpty(array)) { for (String str : array) { if (collection.contains(str)) { return true; } } - return false; } + return false; } /** diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java index e95e3dc0..3870fb90 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/consumer/DevicePropFetchConsumer.java @@ -99,7 +99,7 @@ public class DevicePropFetchConsumer { // 不进行格式校验,直接转换数据 byte[] commandData = ByteUtils.convertToHexByteArray(command); switch (serverType) { - //通过mqtt内部客户端 下发指令 + // 通过mqtt内部客户端 下发指令 case MQTT: publish(topic, commandData); log.info("=>MQTT-线程=[{}],设备=[{}],轮询指令:[{}],主题:[{}]", Thread.currentThread().getName(), serialNumber, command, topic); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java index 356168fb..b3b31169 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceAbstractQuartzJob.java @@ -30,7 +30,7 @@ public abstract class DeviceAbstractQuartzJob implements Job { /** * 线程本地变量 */ - private static ThreadLocal threadLocal = new ThreadLocal<>(); + private static final ThreadLocal threadLocal = new ThreadLocal<>(); @Override public void execute(JobExecutionContext context) throws JobExecutionException { @@ -38,9 +38,7 @@ public abstract class DeviceAbstractQuartzJob implements Job { BeanUtils.copyBeanProp(deviceJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); try { before(context, deviceJob); - if (deviceJob != null) { - doExecute(context, deviceJob); - } + doExecute(context, deviceJob); after(context, deviceJob, null); } catch (Exception e) { log.error("任务执行异常 - :", e); @@ -92,10 +90,6 @@ public abstract class DeviceAbstractQuartzJob implements Job { startTime = new Date(); } sysJobLog.setStartTime(startTime); - Date stopTime = new Date(); - sysJobLog.setStopTime(stopTime); - long runMs = stopTime.getTime() - startTime.getTime(); - sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); if (e != null) { sysJobLog.setStatus(Constants.FAIL); String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); @@ -104,6 +98,11 @@ public abstract class DeviceAbstractQuartzJob implements Job { sysJobLog.setStatus(Constants.SUCCESS); } + Date stopTime = new Date(); + sysJobLog.setStopTime(stopTime); + long runMs = stopTime.getTime() - startTime.getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + // 写入数据库当中 SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java index 62c5bb74..e45a4196 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceDisallowConcurrentExecution.java @@ -11,6 +11,7 @@ import org.quartz.JobExecutionContext; */ @DisallowConcurrentExecution public class DeviceDisallowConcurrentExecution extends DeviceAbstractQuartzJob { + @Override protected void doExecute(JobExecutionContext context, DeviceJob deviceJob) throws Exception { DeviceJobInvoke.invokeMethod(deviceJob); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java index 87788c0b..5f9af7c2 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobExecution.java @@ -9,6 +9,7 @@ import org.quartz.JobExecutionContext; * @author sumuxi, 2026-04-16 */ public class DeviceJobExecution extends DeviceAbstractQuartzJob { + @Override protected void doExecute(JobExecutionContext context, DeviceJob deviceJob) throws Exception { DeviceJobInvoke.invokeMethod(deviceJob); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java index 7f0f4332..cff7e810 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/DeviceJobInvoke.java @@ -29,13 +29,13 @@ public class DeviceJobInvoke { /** * 获取消息推送接口 */ - private static IMqttMessagePublish messagePublish = SpringUtils.getBean(IMqttMessagePublish.class); + private static final IMqttMessagePublish messagePublish = SpringUtils.getBean(IMqttMessagePublish.class); - private static SceneMapper sceneMapper = SpringUtils.getBean(SceneMapper.class); + private static final SceneMapper sceneMapper = SpringUtils.getBean(SceneMapper.class); - private static FlowLogExecutor flowExecutor = SpringUtils.getBean(FlowLogExecutor.class); + private static final FlowLogExecutor flowExecutor = SpringUtils.getBean(FlowLogExecutor.class); - private static IDataHandler dataHandler = SpringUtils.getBean(IDataHandler.class); + private static final IDataHandler dataHandler = SpringUtils.getBean(IDataHandler.class); /** * 执行方法 @@ -44,45 +44,45 @@ public class DeviceJobInvoke { */ public static void invokeMethod(DeviceJob deviceJob) throws Exception { if (deviceJob.getJobType() == 1) { - log.info("------------------------1、执行设备定时任务-----------------------------"); + log.info("1、[执行设备定时任务]"); List actions = JSON.parseArray(deviceJob.getActions(), Action.class); - List propertys = new ArrayList<>(); + List properties = new ArrayList<>(); List functions = new ArrayList<>(); - for (int i = 0; i < actions.size(); i++) { + for (Action action : actions) { ThingsModelSimpleItem model = new ThingsModelSimpleItem(); - model.setId(actions.get(i).getId()); - model.setValue(actions.get(i).getValue()); + model.setId(action.getId()); + model.setValue(action.getValue()); model.setRemark("设备定时"); - if (actions.get(i).getType() == 1) { - propertys.add(model); - } else if (actions.get(i).getType() == 2) { + if (action.getType() == 1) { + properties.add(model); + } else if (action.getType() == 2) { functions.add(model); } } // 发布属性 - if (propertys.size() > 0) { - messagePublish.publishProperty(deviceJob.getProductId(), deviceJob.getSerialNumber(), propertys, 0); + if (!properties.isEmpty()) { + messagePublish.publishProperty(deviceJob.getProductId(), deviceJob.getSerialNumber(), properties, 0); } // 发布功能 - if (functions.size() > 0) { + if (!functions.isEmpty()) { messagePublish.publishFunction(deviceJob.getProductId(), deviceJob.getSerialNumber(), functions, 0); } } else if (deviceJob.getJobType() == 3) { - log.info("------------------3、[定时执行场景联动]---------------------"); + log.info("3、[定时执行场景联动]"); Scene scene = sceneMapper.selectSceneBySceneId(deviceJob.getSceneId()); // 执行场景规则,异步非阻塞 SceneContext context = new SceneContext("", 0L, 0, null); String requestId = "scene/" + scene.getChainName(); flowExecutor.execute2FutureWithRid(scene.getChainName(), null, requestId, context); } else if (4 == deviceJob.getJobType()) { - log.info("------------------4、[定时执行场景运算型变量]---------------------"); + log.info("4、[定时执行场景运算型变量]"); String s = dataHandler.calculateSceneModelTagValue(deviceJob.getDatasourceId()); if (StringUtils.isEmpty(s)) { - log.info("------------------[定时执行场景运算型变量失败:+{}]---------------------", s); + log.info("[定时执行场景运算型变量失败:+{}]", s); } } else if (5 == deviceJob.getJobType()) { - log.info("----------------5、执行modbus轮训指令----------------------"); + log.info("5、[执行Modbus轮训指令]"); ModbusPollJob job = new ModbusPollJob(); job.setType(2); job.setDeviceJob(deviceJob); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java index 6f113113..5b299e10 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/job/SyncDeviceStatusJob.java @@ -38,7 +38,7 @@ public class SyncDeviceStatusJob { */ public void syncDeviceStatus() { if (enabled) { - //获取所有已激活并不是禁用的设备 + // 获取所有已激活并不是禁用的设备 List deviceStatusVOList = deviceService.selectDeviceActive(); if (!CollectionUtils.isEmpty(deviceStatusVOList)) { for (DeviceStatusVO statusVO : deviceStatusVOList) { diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java index 8f3cf966..9109c799 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceReplyListen.java @@ -40,6 +40,9 @@ public class DeviceReplyListen { } } catch (Exception e) { log.error("=>设备回调消息监听异常", e); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java index 072351c0..c3cd81e8 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/DeviceTestListen.java @@ -37,6 +37,9 @@ public class DeviceTestListen { } } catch (Exception e) { log.error("=>emq数据转发异常"); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java index 8f24f30e..aa4473ae 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/listen/FunctionInvokeListen.java @@ -38,6 +38,9 @@ public class FunctionInvokeListen { } } catch (Exception e) { log.error("=>下发服务消费异常", e); + } finally { + // 确保任何情况下都清理上下文 + TraceContext.clear(); } } } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java index 687a9dbb..fe52ccfb 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceJobServiceImpl.java @@ -47,6 +47,10 @@ public class DeviceJobServiceImpl implements IDeviceJobService { @PostConstruct public void init() throws SchedulerException, TaskException { scheduler.clear(); + // 打印实际类型 +// log.info("Scheduler实际类型: {}", scheduler.getClass().getName()); +// log.info("Scheduler元数据: {}", scheduler.getMetaData().getSummary()); +// log.info("Scheduler class: {}", scheduler.getClass()); // 设备定时任务 List jobList = jobMapper.selectJobAll(); for (DeviceJob deviceJob : jobList) { diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java index 009c6e8b..20f45d25 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceMessageServiceImpl.java @@ -222,7 +222,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { /** * 变量读取 * - * @param readVO + * @param readVO 前端入参 */ @Override public void readVariableValue(VariableReadVO readVO) { @@ -233,7 +233,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { deviceAndProtocol.getProtocolCode().equals(FastBeeConstant.PROTOCOL.ModbusTcp))) throw new ServiceException("非modbus协议请先配置主动采集协议"); Long productId = deviceAndProtocol.getProductId(); - //如果是子设备,则获取网关子设备的产品id和设备编号 + // 如果是子设备,则获取网关子设备的产品id和设备编号 Integer deviceType = deviceAndProtocol.getDeviceType(); if (deviceType == DeviceType.SUB_GATEWAY.getCode()) { serialNumber = deviceAndProtocol.getGwSerialNumber(); @@ -242,7 +242,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { Integer type = readVO.getType(); type = Objects.isNull(type) ? 1 : type; if (type == 1) { - //单个变量获取 + // 单个变量获取 String identifier = readVO.getIdentifier(); ThingsModelValueItem thingModels = itslCache.getSingleThingModels(deviceAndProtocol.getProductId(), identifier); ModbusConfig config = thingModels.getConfig(); @@ -275,7 +275,7 @@ public class DeviceMessageServiceImpl implements IDeviceMessageService { job.setType(1); MessageProducer.sendPropFetch(job); } else { - //读取当前设备的所有变量,这里读取所有的,判断传递的设备是网关设备、网关子设备、直连设备 + // 读取当前设备的所有变量,这里读取所有的,判断传递的设备是网关设备、网关子设备、直连设备 DeviceType code = DeviceType.transfer(deviceType); List modbusJobList = modbusJobService.selectDevicesJobByDeviceType(code, readVO.getSerialNumber()); List commandList = modbusJobList.stream().map(ModbusJob::getCommand).collect(Collectors.toList()); diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java index b6f01656..bf0ddd6e 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/controller/SysJobController.java @@ -83,9 +83,9 @@ public class SysJobController extends BaseController { return error(StringUtils.format(MessageUtils.message("job.add.failed.cron.not.valid"), job.getJobName())); } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.rmi.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.ldap.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.HTTP, Constants.HTTPS)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.http.not.valid"), job.getJobName())); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { return error(StringUtils.format(MessageUtils.message("job.add.failed.string.error"), job.getJobName())); @@ -108,9 +108,9 @@ public class SysJobController extends BaseController { return error(StringUtils.format(MessageUtils.message("job.update.failed.cron.not.valid"), job.getJobName())); } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.rmi.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.ldap.not.valid"), job.getJobName())); - } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.HTTP, Constants.HTTPS)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.http.not.valid"), job.getJobName())); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { return error(StringUtils.format(MessageUtils.message("job.update.failed.string.error"), job.getJobName())); diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java index d7775ee2..30c6f2fe 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/AbstractQuartzJob.java @@ -30,7 +30,7 @@ public abstract class AbstractQuartzJob implements Job { /** * 线程本地变量 */ - private static ThreadLocal threadLocal = new ThreadLocal<>(); + private static final ThreadLocal threadLocal = new ThreadLocal<>(); @Override public void execute(JobExecutionContext context) throws JobExecutionException { @@ -38,9 +38,7 @@ public abstract class AbstractQuartzJob implements Job { BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); try { before(context, sysJob); - if (sysJob != null) { - doExecute(context, sysJob); - } + doExecute(context, sysJob); after(context, sysJob, null); } catch (Exception e) { log.error("任务执行异常 - :", e); @@ -78,9 +76,6 @@ public abstract class AbstractQuartzJob implements Job { sysJobLog.setJobGroup(sysJob.getJobGroup()); sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); sysJobLog.setStartTime(startTime); - sysJobLog.setStopTime(new Date()); - long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); - sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); if (e != null) { sysJobLog.setStatus(Constants.FAIL); String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); @@ -88,14 +83,17 @@ public abstract class AbstractQuartzJob implements Job { } else { sysJobLog.setStatus(Constants.SUCCESS); } - + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); // 写入数据库当中 SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + log.info("定时任务执行完成: {} - {}, 耗时: {}ms", sysJob.getJobName(), sysJob.getJobGroup(), runMs); + // 清除 TraceContext TraceContext.clear(); - log.info("定时任务执行完成: {} - {}, 耗时: {}ms", sysJob.getJobName(), sysJob.getJobGroup(), runMs); } /** diff --git a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java index 1e78616a..02a36297 100644 --- a/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java +++ b/fastbee-plugs/fastbee-quartz/src/main/java/com/fastbee/quartz/util/JobInvokeUtil.java @@ -99,8 +99,8 @@ public class JobInvokeUtil { } String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); List classs = new LinkedList<>(); - for (int i = 0; i < methodParams.length; i++) { - String str = StringUtils.trimToEmpty(methodParams[i]); + for (String methodParam : methodParams) { + String str = StringUtils.trimToEmpty(methodParam); // String字符串类型,以'或"开头 if (StringUtils.startsWithAny(str, "'", "\"")) { classs.add(new Object[]{StringUtils.substring(str, 1, str.length() - 1), String.class}); @@ -151,7 +151,7 @@ public class JobInvokeUtil { Object[] classs = new Object[methodParams.size()]; int index = 0; for (Object[] os : methodParams) { - classs[index] = (Object) os[0]; + classs[index] = os[0]; index++; } return classs; From 20a148a04ea5d875ae2626d81b09cd3fc2ad5218 Mon Sep 17 00:00:00 2001 From: zhugongping Date: Wed, 16 Sep 2026 19:28:16 +0800 Subject: [PATCH 14/19] =?UTF-8?q?=E4=B8=93=E9=A1=B9=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=BB=91=E5=AE=9A=E9=A1=B5=E9=9D=A2=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlatformDeviceMappingController.java | 7 +++++ .../PlatformDeviceSubMappingMapper.java | 3 +++ .../IPlatformDeviceSubMappingService.java | 4 +++ .../PlatformDeviceMappingServiceImpl.java | 25 +++++++++++++++++- .../PlatformDeviceSubMappingServiceImpl.java | 23 ++++++++++++++++ .../PlatformDeviceSubMappingMapper.xml | 10 +++++++ yack-web/src/api/platform/deviceMapping.js | 5 ++++ .../views/platform/device-mapping/index.vue | 26 ++++++------------- 8 files changed, 84 insertions(+), 19 deletions(-) diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java index ed857816..306bae39 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformDeviceMappingController.java @@ -75,6 +75,13 @@ public class PlatformDeviceMappingController extends BaseController { // ==================== 子映射管理 ==================== + @PreAuthorize("@ss.hasAnyPermi('platform:deviceMapping:query,platform:deviceMapping:list')") + @GetMapping("/sub-mappings/batch") + @ApiOperation("批量查询多个设备绑定的子设备映射(列表页展开行一次拉取,避免 N 次请求)") + public AjaxResult listSubMappingsBatch(@RequestParam List parentIds) { + return success(subMappingService.selectByParentIds(parentIds)); + } + @PreAuthorize("@ss.hasAnyPermi('platform:deviceMapping:query,platform:deviceMapping:list')") @GetMapping("/{id}/sub-mappings") @ApiOperation("查询子设备映射列表") diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java index 588134cb..0ef9ba03 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformDeviceSubMappingMapper.java @@ -10,6 +10,9 @@ public interface PlatformDeviceSubMappingMapper extends BaseMapperX selectByParentId(@Param("parentMappingId") Long parentMappingId); + /** 批量查询多个父映射下的所有子映射(IN 查询,列表页展开行使用) */ + List selectByParentIds(@Param("parentIds") List parentIds); + PlatformDeviceSubMapping selectByParentAndSerial(@Param("parentMappingId") Long parentMappingId, @Param("subSerialNumber") String subSerialNumber); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java index 9eeacffe..23c48331 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformDeviceSubMappingService.java @@ -4,12 +4,16 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; import java.util.List; +import java.util.Map; public interface IPlatformDeviceSubMappingService extends IService { /** 查询父映射下的所有子映射 */ List selectByParentId(Long parentMappingId); + /** 批量查询多个父映射下的子映射,按 parentMappingId 分组返回(列表页展开行一次拉取,避免 N 次请求) */ + Map> selectByParentIds(List parentIds); + /** 根据子设备序列号查找子映射(推送时使用) */ List selectBySubSerialNumber(String subSerialNumber); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java index d4bb4c03..895a1e54 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java @@ -3,6 +3,8 @@ package com.fastbee.iot.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fastbee.iot.domain.Device; +import com.fastbee.iot.mapper.DeviceMapper; import com.fastbee.iot.platform.domain.*; import com.fastbee.iot.platform.flow.FlowContext; import com.fastbee.iot.platform.flow.PlatformFlowExecutor; @@ -21,6 +23,7 @@ import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -41,6 +44,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl selectList(PlatformDeviceMapping query) { @@ -58,7 +63,25 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl rows = this.list(wrapper); + // 批量补全设备名称:device_name 为冗余字段,旧数据可能为空,按 deviceId 一次 IN 查询回填,避免前端逐个查询 + List deviceIds = rows.stream() + .filter(m -> m.getDeviceId() != null && !StringUtils.hasText(m.getDeviceName())) + .map(PlatformDeviceMapping::getDeviceId) + .distinct() + .collect(Collectors.toList()); + if (!deviceIds.isEmpty()) { + List devices = deviceMapper.selectList(Wrappers.lambdaQuery() + .in(Device::getDeviceId, deviceIds)); + Map nameMap = devices.stream() + .collect(Collectors.toMap(Device::getDeviceId, Device::getDeviceName, (a, b) -> a)); + rows.forEach(m -> { + if (m.getDeviceId() != null && !StringUtils.hasText(m.getDeviceName())) { + m.setDeviceName(nameMap.get(m.getDeviceId())); + } + }); + } + return rows; } @Override diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java index 8e331fbe..873b6e0d 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceSubMappingServiceImpl.java @@ -8,7 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -24,6 +28,25 @@ public class PlatformDeviceSubMappingServiceImpl extends ServiceImpl> selectByParentIds(List parentIds) { + if (parentIds == null || parentIds.isEmpty()) { + return Collections.emptyMap(); + } + List all = subMappingMapper.selectByParentIds(parentIds); + Map> grouped = new HashMap<>(); + for (PlatformDeviceSubMapping sub : all) { + Long pid = sub.getParentMappingId(); + if (pid == null) continue; + grouped.computeIfAbsent(pid, k -> new ArrayList<>()).add(sub); + } + // 保证请求的每个 parentId 都有 key,即便没有子映射也返回空列表,避免前端取到 undefined + for (Long pid : parentIds) { + grouped.putIfAbsent(pid, new ArrayList<>()); + } + return grouped; + } + @Override public List selectBySubSerialNumber(String subSerialNumber) { return subMappingMapper.selectBySubSerialNumber(subSerialNumber); diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceSubMappingMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceSubMappingMapper.xml index 783cc576..12a7d254 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceSubMappingMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceSubMappingMapper.xml @@ -20,4 +20,14 @@ WHERE sub_serial_number = #{subSerialNumber} + + + diff --git a/yack-web/src/api/platform/deviceMapping.js b/yack-web/src/api/platform/deviceMapping.js index 380ee19f..920f19e4 100644 --- a/yack-web/src/api/platform/deviceMapping.js +++ b/yack-web/src/api/platform/deviceMapping.js @@ -28,6 +28,11 @@ export function listSubMappings(parentId) { return request({ url: `/platform/device-mapping/${parentId}/sub-mappings`, method: 'get' }) } +// 批量查询多个设备绑定的子设备映射,列表页展开行一次拉取,避免 N 次请求 +export function listSubMappingsBatch(parentIds) { + return request({ url: '/platform/device-mapping/sub-mappings/batch', method: 'get', params: { parentIds: parentIds.join(',') } }) +} + export function syncSubMappings(parentId, data) { return request({ url: `/platform/device-mapping/${parentId}/sub-mappings`, method: 'post', data }) } diff --git a/yack-web/src/views/platform/device-mapping/index.vue b/yack-web/src/views/platform/device-mapping/index.vue index 0b5f70e8..2a4aaa49 100644 --- a/yack-web/src/views/platform/device-mapping/index.vue +++ b/yack-web/src/views/platform/device-mapping/index.vue @@ -335,7 +335,7 @@