From d4ad1eab67c720e486206621689817b2ad07ea41 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 10:42:29 +0800 Subject: [PATCH] =?UTF-8?q?20260530:=E9=9C=80=E6=B1=82=E7=AC=AC2=E7=89=88?= =?UTF-8?q?=E6=8E=A2=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 8 + .../platform/PlatformApiController.java | 8 + .../platform/PlatformBindingController.java | 13 +- .../PlatformTenantChannelController.java | 11 + .../platform/service/IPlatformApiService.java | 3 + .../service/IPlatformBindingService.java | 6 + .../IPlatformTenantChannelService.java | 6 + .../platform/service/PlatformHttpClient.java | 77 +++-- .../service/PlatformTokenService.java | 44 +++ .../service/impl/PlatformApiServiceImpl.java | 48 +++ .../impl/PlatformBindingServiceImpl.java | 72 ++++- .../PlatformTenantChannelServiceImpl.java | 93 +++++- sql/iot_platform_connect_menu_upgrade.sql | 20 ++ yack-web/src/api/iot/platformConnect/api.js | 9 + .../src/api/iot/platformConnect/binding.js | 4 + .../api/iot/platformConnect/tenantChannel.js | 4 + yack-web/src/assets/icons/svg/api.svg | 1 + .../views/iot/platformConnect/api/index.vue | 72 ++++- .../binding/FieldMappingDialog.vue | 60 +++- .../iot/platformConnect/binding/index.vue | 207 +++++++------ .../components/HttpRequestDesigner.vue | 162 ++++++++-- .../components/KeyValueEditor.vue | 287 ++++++++++++++++-- .../platformConnect/tenantChannel/index.vue | 139 +++++++-- .../platformConnect/utils/requestTemplate.js | 49 ++- 24 files changed, 1190 insertions(+), 213 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 sql/iot_platform_connect_menu_upgrade.sql create mode 100644 yack-web/src/assets/icons/svg/api.svg diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..9a8e9dae --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(python3 -c \"import pdfplumber; pdf = pdfplumber.open\\('D:/SoftProject/Java/fastbee/docs/platform/DCP接口文档\\(轴力计\\).pdf'\\); [print\\(p.extract_text\\(\\)\\) for p in pdf.pages[:15]]; pdf.close\\(\\)\")", + "Bash(mvn compile *)" + ] + } +} diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformApiController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformApiController.java index 01b39d2e..005ac305 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformApiController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformApiController.java @@ -64,4 +64,12 @@ public class PlatformApiController extends BaseController { public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(platformApiService.deletePlatformApiByIds(ids)); } + + @PreAuthorize("@ss.hasPermi('iot:platformConnect:api:edit')") + @PostMapping("/test") + @ApiOperation("测试接口连通性(使用当前表单数据,不依赖已保存记录)") + public AjaxResult test(@RequestBody PlatformApi platformApi, + @RequestParam(required = false) Long tenantChannelId) { + return success(platformApiService.testApi(platformApi, tenantChannelId)); + } } diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformBindingController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformBindingController.java index 7e3e89a2..cb7f1e0b 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformBindingController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformBindingController.java @@ -44,7 +44,11 @@ public class PlatformBindingController extends BaseController { @PostMapping @ApiOperation("新增设备绑定") public AjaxResult add(@RequestBody PlatformBinding binding) { - return toAjax(platformBindingService.insertPlatformBinding(binding)); + Boolean result = platformBindingService.insertPlatformBinding(binding); + if (result) { + return success(binding.getId()); + } + return error("新增失败"); } @PreAuthorize("@ss.hasPermi('iot:platformConnect:binding:edit')") @@ -62,4 +66,11 @@ public class PlatformBindingController extends BaseController { public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(platformBindingService.deletePlatformBindingByIds(ids)); } + + @PreAuthorize("@ss.hasPermi('iot:platformConnect:binding:edit')") + @PostMapping("/generateDefaultMappings") + @ApiOperation("根据设备类型生成默认字段映射") + public AjaxResult generateDefaultMappings(@RequestParam Long bindingId, @RequestParam String deviceType) { + return success(platformBindingService.generateDefaultMappings(bindingId, deviceType)); + } } diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformTenantChannelController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformTenantChannelController.java index 14951d11..2ee780dd 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformTenantChannelController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformTenantChannelController.java @@ -64,4 +64,15 @@ public class PlatformTenantChannelController extends BaseController { public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(platformTenantChannelService.deletePlatformTenantChannelByIds(ids)); } + + @PreAuthorize("@ss.hasPermi('iot:platformConnect:tenantChannel:edit')") + @PostMapping("/fetchRemoteInfo") + @ApiOperation("获取第三方平台远程信息(用户ID、项目列表)") + public AjaxResult fetchRemoteInfo(@RequestBody PlatformTenantChannel tenantChannel) { + try { + return success(platformTenantChannelService.fetchRemoteInfo(tenantChannel)); + } catch (Exception e) { + return error(e.getMessage()); + } + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformApiService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformApiService.java index 79e8b1ca..27e02243 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformApiService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformApiService.java @@ -2,6 +2,7 @@ package com.fastbee.iot.platform.service; import com.baomidou.mybatisplus.extension.service.IService; import com.fastbee.iot.platform.domain.PlatformApi; +import com.fastbee.iot.platform.model.PlatformHttpResponse; import java.util.List; @@ -15,4 +16,6 @@ public interface IPlatformApiService extends IService { Boolean updatePlatformApi(PlatformApi entity); Boolean deletePlatformApiByIds(Long[] ids); + + PlatformHttpResponse testApi(PlatformApi api, Long tenantChannelId); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformBindingService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformBindingService.java index 69acb824..30b4f486 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformBindingService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformBindingService.java @@ -2,6 +2,7 @@ package com.fastbee.iot.platform.service; import com.baomidou.mybatisplus.extension.service.IService; import com.fastbee.iot.platform.domain.PlatformBinding; +import com.fastbee.iot.platform.domain.PlatformFieldMapping; import java.util.List; @@ -15,4 +16,9 @@ public interface IPlatformBindingService extends IService { Boolean updatePlatformBinding(PlatformBinding entity); Boolean deletePlatformBindingByIds(Long[] ids); + + /** + * 根据设备类型生成默认字段映射 + */ + List generateDefaultMappings(Long bindingId, String deviceType); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformTenantChannelService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformTenantChannelService.java index d32a3d68..bf1c12df 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformTenantChannelService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformTenantChannelService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.fastbee.iot.platform.domain.PlatformTenantChannel; import java.util.List; +import java.util.Map; public interface IPlatformTenantChannelService extends IService { List selectPlatformTenantChannelList(PlatformTenantChannel query); @@ -15,4 +16,9 @@ public interface IPlatformTenantChannelService extends IService fetchRemoteInfo(PlatformTenantChannel tenantChannel); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformHttpClient.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformHttpClient.java index d4fecb17..ec0d19aa 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformHttpClient.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformHttpClient.java @@ -101,25 +101,7 @@ public class PlatformHttpClient { // 7. 设置 Body String contentType = StringUtils.isNotEmpty(api.getContentType()) ? api.getContentType() : "application/json"; - String body = buildBody(api.getBodyTemplate(), contentType, variables); - if (StringUtils.isNotEmpty(body)) { - if (contentType.contains("json")) { - request.addHeader("Content-Type", "application/json"); - request.addBody(body); - } else if (contentType.contains("form")) { - request.addHeader("Content-Type", "application/x-www-form-urlencoded"); - JSONObject formData = JSON.parseObject(body); - if (formData != null) { - for (Map.Entry entry : formData.entrySet()) { - request.addBody(entry.getKey(), entry.getValue()); - } - } - } else { - request.addHeader("Content-Type", contentType); - request.addBody(body); - } - result.setRequestBody(body); - } + applyBody(request, api.getBodyTemplate(), contentType, variables, result); // 8. 执行请求 ForestResponse response = request.execute(ForestResponse.class); @@ -170,7 +152,62 @@ public class PlatformHttpClient { } /** - * 构建请求体 + * 解析 bodyTemplate 并设置到请求中 + * bodyTemplate 可能是: + * 1. null/空 — 无 body + * 2. 带 mode 字段的 JSON:{"mode":"x-www-form-urlencoded","urlencoded":[...]} 或 {"mode":"form-data","formData":[...]} + * 3. 普通 JSON 字符串(raw-json 模式直接存的原始内容) + */ + private void applyBody(ForestRequest request, String bodyTemplate, String contentType, + Map variables, PlatformHttpResponse result) { + if (StringUtils.isEmpty(bodyTemplate)) { + return; + } + try { + JSONObject parsed = JSON.parseObject(bodyTemplate); + String mode = parsed != null ? parsed.getString("mode") : null; + if ("x-www-form-urlencoded".equals(mode) || "form-data".equals(mode)) { + // 取 urlencoded 或 formData 数组,转为 key-value 表单参数 + String arrayKey = "x-www-form-urlencoded".equals(mode) ? "urlencoded" : "formData"; + com.alibaba.fastjson2.JSONArray rows = parsed.getJSONArray(arrayKey); + if (rows != null && !rows.isEmpty()) { + request.addHeader("Content-Type", "application/x-www-form-urlencoded"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < rows.size(); i++) { + JSONObject row = rows.getJSONObject(i); + if (row == null) continue; + Boolean enabled = row.getBoolean("enabled"); + if (Boolean.FALSE.equals(enabled)) continue; + String key = row.getString("key"); + String value = row.getString("value"); + if (StringUtils.isEmpty(key)) continue; + value = substituteVariables(value != null ? value : "", variables); + request.addBody(key, value); + if (sb.length() > 0) sb.append('&'); + sb.append(key).append('=').append(value); + } + result.setRequestBody(sb.toString()); + } + return; + } + } catch (Exception ignored) { + // 不是 JSON 或无 mode 字段,按 raw 处理 + } + // raw-json 或普通文本 + String body = substituteVariables(bodyTemplate, variables); + if (StringUtils.isNotEmpty(body)) { + if (contentType.contains("json")) { + request.addHeader("Content-Type", "application/json"); + } else { + request.addHeader("Content-Type", contentType); + } + request.addBody(body); + result.setRequestBody(body); + } + } + + /** + * 构建请求体(保留兼容旧调用,内部不再使用) */ private String buildBody(String bodyTemplate, String contentType, Map variables) { if (StringUtils.isEmpty(bodyTemplate)) { diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformTokenService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformTokenService.java index adb0b84f..9bd4be24 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformTokenService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/PlatformTokenService.java @@ -60,6 +60,12 @@ public class PlatformTokenService { if (authConfig.containsKey("expiresIn")) { expiresIn = authConfig.getIntValue("expiresIn"); } + } else if ("oauth2_password".equals(authType)) { + JSONObject result2 = acquireOAuth2PasswordToken(tenantChannel.getBaseUrl(), authConfig); + token = result2.getString("token"); + if (result2.containsKey("expiresIn")) { + expiresIn = result2.getIntValue("expiresIn"); + } } else if ("custom_token".equals(authType)) { JSONObject result = acquireCustomToken(tenantChannel.getBaseUrl(), authConfig); token = result.getString("token"); @@ -178,6 +184,44 @@ public class PlatformTokenService { throw new RuntimeException("自定义 token 获取失败: " + response.getContent()); } + /** + * OAuth2 password 模式获取 token(DCP 风格:form-urlencoded,grant_type=password) + * authConfig 需包含: tokenUrl, userName, password, clientId, scope + */ + private JSONObject acquireOAuth2PasswordToken(String baseUrl, JSONObject authConfig) { + String tokenUrl = resolveTokenUrl(baseUrl, authConfig.getString("tokenUrl")); + String clientId = authConfig.getString("clientId"); + String userName = authConfig.getString("userName"); + String password = authConfig.getString("password"); + String scope = authConfig.getString("scope"); + + ForestRequest request = Forest.request(); + request.setUrl(tokenUrl); + request.setType(ForestRequestType.POST); + request.addHeader("Content-Type", "application/x-www-form-urlencoded"); + request.addBody("grant_type", "password"); + if (StringUtils.isNotEmpty(clientId)) { + request.addBody("client_id", clientId); + } + if (StringUtils.isNotEmpty(scope)) { + request.addBody("scope", scope); + } + request.addBody("userName", userName); + request.addBody("password", password); + + ForestResponse response = request.execute(ForestResponse.class); + if (response.isSuccess()) { + JSONObject body = JSON.parseObject(response.getContent()); + JSONObject result = new JSONObject(); + result.put("token", body.getString("access_token")); + if (body.containsKey("expires_in")) { + result.put("expiresIn", body.getIntValue("expires_in")); + } + return result; + } + throw new RuntimeException("OAuth2 password token 获取失败: " + response.getContent()); + } + /** * 解析 authConfig JSON */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformApiServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformApiServiceImpl.java index eeb7c613..0befef33 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformApiServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformApiServiceImpl.java @@ -6,16 +6,33 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.iot.platform.domain.PlatformApi; +import com.fastbee.iot.platform.domain.PlatformTemplate; +import com.fastbee.iot.platform.domain.PlatformTenantChannel; import com.fastbee.iot.platform.mapper.PlatformApiMapper; +import com.fastbee.iot.platform.model.PlatformHttpResponse; import com.fastbee.iot.platform.service.IPlatformApiService; +import com.fastbee.iot.platform.service.IPlatformTemplateService; +import com.fastbee.iot.platform.service.IPlatformTenantChannelService; +import com.fastbee.iot.platform.service.PlatformHttpClient; import org.springframework.stereotype.Service; +import javax.annotation.Resource; import java.util.Arrays; +import java.util.Collections; import java.util.List; @Service public class PlatformApiServiceImpl extends ServiceImpl implements IPlatformApiService { + @Resource + private IPlatformTemplateService platformTemplateService; + + @Resource + private IPlatformTenantChannelService platformTenantChannelService; + + @Resource + private PlatformHttpClient platformHttpClient; + @Override public List selectPlatformApiList(PlatformApi query) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); @@ -50,4 +67,35 @@ public class PlatformApiServiceImpl extends ServiceImpl lqw = Wrappers.lambdaQuery(); + lqw.eq(PlatformTenantChannel::getTemplateId, api.getTemplateId()); + lqw.last("LIMIT 1"); + channel = platformTenantChannelService.getOne(lqw); + } + if (channel == null) { + channel = new PlatformTenantChannel(); + channel.setBaseUrl(template.getBaseUrlTemplate()); + } + return platformHttpClient.call(channel, template, api, Collections.emptyMap()); + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformBindingServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformBindingServiceImpl.java index 13d51543..460ca7b2 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformBindingServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformBindingServiceImpl.java @@ -6,16 +6,21 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.iot.platform.domain.PlatformBinding; +import com.fastbee.iot.platform.domain.PlatformFieldMapping; import com.fastbee.iot.platform.mapper.PlatformBindingMapper; +import com.fastbee.iot.platform.mapper.PlatformFieldMappingMapper; import com.fastbee.iot.platform.service.IPlatformBindingService; import org.springframework.stereotype.Service; -import java.util.Arrays; -import java.util.List; +import javax.annotation.Resource; +import java.util.*; @Service public class PlatformBindingServiceImpl extends ServiceImpl implements IPlatformBindingService { + @Resource + private PlatformFieldMappingMapper fieldMappingMapper; + @Override public List selectPlatformBindingList(PlatformBinding query) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); @@ -56,4 +61,67 @@ public class PlatformBindingServiceImpl extends ServiceImpl generateDefaultMappings(Long bindingId, String deviceType) { + // 先检查是否已有映射 + LambdaQueryWrapper existQuery = Wrappers.lambdaQuery(); + existQuery.eq(PlatformFieldMapping::getBindingId, bindingId); + Long existCount = fieldMappingMapper.selectCount(existQuery); + if (existCount > 0) { + // 已有映射,直接返回现有列表 + return fieldMappingMapper.selectList(existQuery); + } + + // 根据设备类型生成默认映射 + List mappings = buildDefaultMappings(deviceType); + int sortOrder = 1; + for (PlatformFieldMapping mapping : mappings) { + mapping.setBindingId(bindingId); + mapping.setDirection("OUTBOUND"); + mapping.setSortOrder(sortOrder++); + mapping.setStatus(1); + fieldMappingMapper.insert(mapping); + } + return mappings; + } + + private List buildDefaultMappings(String deviceType) { + List list = new ArrayList<>(); + switch (deviceType) { + case "RAIN": + list.add(createMapping("THING_MODEL", "rainfall", "累计雨量", "value", "雨量值", "DIRECT")); + list.add(createMapping("THING_MODEL", "temperature", "温度", "temperature", "温度", "DIRECT")); + break; + case "TILT": + list.add(createMapping("THING_MODEL", "xAngle", "X轴角度", "xDeg", "X轴角度", "DIRECT")); + list.add(createMapping("THING_MODEL", "yAngle", "Y轴角度", "yDeg", "Y轴角度", "DIRECT")); + list.add(createMapping("THING_MODEL", "zAngle", "Z轴角度", "zDeg", "Z轴角度", "DIRECT")); + list.add(createMapping("THING_MODEL", "temperature", "温度", "temperature", "温度", "DIRECT")); + break; + case "STATIC_LEVEL": + list.add(createMapping("THING_MODEL", "height", "液位高度", "height", "液位高度", "DIRECT")); + list.add(createMapping("THING_MODEL", "temperature", "温度", "temperature", "温度", "DIRECT")); + break; + case "AXIAL_FORCE": + list.add(createMapping("THING_MODEL", "strain", "应变值", "e", "应变值", "DIRECT")); + break; + default: + break; + } + return list; + } + + private PlatformFieldMapping createMapping(String sourceType, String sourceKey, String sourceName, + String targetPath, String targetName, String transformType) { + PlatformFieldMapping mapping = new PlatformFieldMapping(); + mapping.setSourceType(sourceType); + mapping.setSourceKey(sourceKey); + mapping.setSourceName(sourceName); + mapping.setTargetPath(targetPath); + mapping.setTargetName(targetName); + mapping.setTransformType(transformType); + mapping.setRequired(1); + return mapping; + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformTenantChannelServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformTenantChannelServiceImpl.java index b6a07e87..314dc16f 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformTenantChannelServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformTenantChannelServiceImpl.java @@ -1,21 +1,39 @@ package com.fastbee.iot.platform.service.impl; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; 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.dtflys.forest.Forest; +import com.dtflys.forest.http.ForestRequest; +import com.dtflys.forest.http.ForestRequestType; +import com.dtflys.forest.http.ForestResponse; import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.StringUtils; +import com.fastbee.iot.platform.domain.PlatformTemplate; import com.fastbee.iot.platform.domain.PlatformTenantChannel; import com.fastbee.iot.platform.mapper.PlatformTenantChannelMapper; import com.fastbee.iot.platform.service.IPlatformTenantChannelService; +import com.fastbee.iot.platform.service.IPlatformTemplateService; +import com.fastbee.iot.platform.service.PlatformTokenService; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.Arrays; -import java.util.List; +import javax.annotation.Resource; +import java.util.*; +@Slf4j @Service public class PlatformTenantChannelServiceImpl extends ServiceImpl implements IPlatformTenantChannelService { + @Resource + private PlatformTokenService platformTokenService; + + @Resource + private IPlatformTemplateService platformTemplateService; + @Override public List selectPlatformTenantChannelList(PlatformTenantChannel query) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); @@ -53,4 +71,75 @@ public class PlatformTenantChannelServiceImpl extends ServiceImpl fetchRemoteInfo(PlatformTenantChannel tenantChannel) { + Map result = new HashMap<>(); + + // 获取模板信息 + PlatformTemplate template = platformTemplateService.selectPlatformTemplateById(tenantChannel.getTemplateId()); + if (template == null) { + throw new RuntimeException("平台模板不存在"); + } + + // 获取 token + String token = platformTokenService.getToken(tenantChannel, template); + if (StringUtils.isEmpty(token)) { + throw new RuntimeException("获取平台 token 失败,请检查凭证配置"); + } + + String baseUrl = StringUtils.isNotEmpty(tenantChannel.getBaseUrl()) ? tenantChannel.getBaseUrl() : template.getBaseUrlTemplate(); + if (StringUtils.isEmpty(baseUrl)) { + throw new RuntimeException("baseUrl 未配置"); + } + baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + + // 1. 获取用户信息 + String userUrl = baseUrl + "/api/app/user/user-id"; + ForestRequest userRequest = Forest.request(); + userRequest.setUrl(userUrl); + userRequest.setType(ForestRequestType.GET); + userRequest.addHeader("Authorization", "Bearer " + token); + ForestResponse userResponse = userRequest.execute(ForestResponse.class); + + if (!userResponse.isSuccess()) { + throw new RuntimeException("获取用户信息失败: " + userResponse.getContent()); + } + + JSONObject userInfo = JSON.parseObject(userResponse.getContent()); + String userId = userInfo.getString("id"); + String userName = userInfo.getString("userName"); + result.put("userId", userId); + result.put("userName", userName); + + // 2. 获取项目列表 + if (StringUtils.isNotEmpty(userId)) { + String projectUrl = baseUrl + "/api/app/project/by-user-id/" + userId; + ForestRequest projectRequest = Forest.request(); + projectRequest.setUrl(projectUrl); + projectRequest.setType(ForestRequestType.GET); + projectRequest.addHeader("Authorization", "Bearer " + token); + ForestResponse projectResponse = projectRequest.execute(ForestResponse.class); + + if (projectResponse.isSuccess()) { + JSONArray projects = JSON.parseArray(projectResponse.getContent()); + List> projectList = new ArrayList<>(); + if (projects != null) { + for (int i = 0; i < projects.size(); i++) { + JSONObject proj = projects.getJSONObject(i); + Map item = new HashMap<>(); + item.put("id", proj.getString("id")); + item.put("name", proj.getString("name")); + projectList.add(item); + } + } + result.put("projects", projectList); + } else { + log.warn("获取项目列表失败: {}", projectResponse.getContent()); + result.put("projects", Collections.emptyList()); + } + } + + return result; + } } diff --git a/sql/iot_platform_connect_menu_upgrade.sql b/sql/iot_platform_connect_menu_upgrade.sql new file mode 100644 index 00000000..727d99ab --- /dev/null +++ b/sql/iot_platform_connect_menu_upgrade.sql @@ -0,0 +1,20 @@ +-- ===================================================================== +-- 平台对接菜单重组:8个平级菜单 → 3个主入口 + 1个"高级配置"子目录 +-- 执行前提:已执行过 iot_platform_connect.sql(菜单已存在) +-- ===================================================================== + +-- 1. 新增"高级配置"目录(menu_id=3580, parent=3500) +INSERT IGNORE INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +VALUES (3580, '高级配置', 3500, 4, 'advanced', NULL, NULL, 1, 0, 'M', '0', '0', '', 'system', 'admin', NOW(), 'admin', NOW(), '平台对接高级配置目录'); + +-- 2. 主入口菜单调整(直接挂在3500下,调整排序和名称) +UPDATE `sys_menu` SET `menu_name` = '连接管理', `order_num` = 1, `icon` = 'peoples' WHERE `menu_id` = 3540; +UPDATE `sys_menu` SET `order_num` = 2 WHERE `menu_id` = 3560; +UPDATE `sys_menu` SET `menu_name` = '推送记录', `order_num` = 3, `icon` = 'log' WHERE `menu_id` = 3510; + +-- 3. 高级配置子菜单(parent_id 改为3580) +UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 1 WHERE `menu_id` = 3501; +UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 2 WHERE `menu_id` = 3520; +UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 3 WHERE `menu_id` = 3530; +UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 4 WHERE `menu_id` = 3550; +UPDATE `sys_menu` SET `parent_id` = 3580, `order_num` = 5 WHERE `menu_id` = 3570; diff --git a/yack-web/src/api/iot/platformConnect/api.js b/yack-web/src/api/iot/platformConnect/api.js index f28cdff5..7cb58b00 100644 --- a/yack-web/src/api/iot/platformConnect/api.js +++ b/yack-web/src/api/iot/platformConnect/api.js @@ -19,3 +19,12 @@ export function updateApi(data) { export function delApi(id) { return request({ url: '/iot/platformConnect/api/' + id, method: 'delete' }) } + +export function testApi(data, tenantChannelId) { + return request({ + url: '/iot/platformConnect/api/test', + method: 'post', + params: tenantChannelId ? { tenantChannelId } : {}, + data: data + }) +} diff --git a/yack-web/src/api/iot/platformConnect/binding.js b/yack-web/src/api/iot/platformConnect/binding.js index 89cc1707..ba296367 100644 --- a/yack-web/src/api/iot/platformConnect/binding.js +++ b/yack-web/src/api/iot/platformConnect/binding.js @@ -19,3 +19,7 @@ export function updateBinding(data) { export function delBinding(id) { return request({ url: '/iot/platformConnect/binding/' + id, method: 'delete' }) } + +export function generateDefaultMappings(bindingId, deviceType) { + return request({ url: '/iot/platformConnect/binding/generateDefaultMappings', method: 'post', params: { bindingId, deviceType } }) +} diff --git a/yack-web/src/api/iot/platformConnect/tenantChannel.js b/yack-web/src/api/iot/platformConnect/tenantChannel.js index 9cbfc126..d278f75f 100644 --- a/yack-web/src/api/iot/platformConnect/tenantChannel.js +++ b/yack-web/src/api/iot/platformConnect/tenantChannel.js @@ -19,3 +19,7 @@ export function updateTenantChannel(data) { export function delTenantChannel(id) { return request({ url: '/iot/platformConnect/tenantChannel/' + id, method: 'delete' }) } + +export function fetchRemoteInfo(data) { + return request({ url: '/iot/platformConnect/tenantChannel/fetchRemoteInfo', method: 'post', data: data }) +} diff --git a/yack-web/src/assets/icons/svg/api.svg b/yack-web/src/assets/icons/svg/api.svg new file mode 100644 index 00000000..f32ed877 --- /dev/null +++ b/yack-web/src/assets/icons/svg/api.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yack-web/src/views/iot/platformConnect/api/index.vue b/yack-web/src/views/iot/platformConnect/api/index.vue index 6926933a..8fa5b47c 100644 --- a/yack-web/src/views/iot/platformConnect/api/index.vue +++ b/yack-web/src/views/iot/platformConnect/api/index.vue @@ -102,21 +102,45 @@ - - + + +
+ + + {{ testResult.requestUrl }} + {{ testResult.statusCode }} + {{ testResult.durationMs }} ms + {{ testResult.errorMessage }} + +
+
请求体
+
{{ formatJson(testResult.requestBody) }}
+
+
+
响应体
+
{{ formatJson(testResult.body) }}
+
+
+ diff --git a/yack-web/src/views/iot/platformConnect/components/HttpRequestDesigner.vue b/yack-web/src/views/iot/platformConnect/components/HttpRequestDesigner.vue index 02fbc38d..24a53915 100644 --- a/yack-web/src/views/iot/platformConnect/components/HttpRequestDesigner.vue +++ b/yack-web/src/views/iot/platformConnect/components/HttpRequestDesigner.vue @@ -1,21 +1,21 @@ @@ -65,8 +98,40 @@ export default { return { activeTab: 'params', methodOptions: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + importJsonDialogVisible: false, + importJsonText: '', + importJsonError: '', }; }, + methods: { + flushEditors() { + ['paramsEditor', 'headersEditor', 'configEditor', 'formDataEditor', 'urlencodedEditor'].forEach(refName => { + const editor = this.$refs[refName]; + if (editor && typeof editor.flushBulk === 'function') { + editor.flushBulk(); + } + }); + }, + confirmImportJson() { + const text = this.importJsonText.trim(); + if (!text) { + this.importJsonError = '内容不能为空'; + return; + } + // 将模板变量临时替换为合法值再做 JSON 校验 + const sanitized = text.replace(/\$\{[^}]+\}/g, '"__var__"'); + try { + JSON.parse(sanitized); + } catch (e) { + this.importJsonError = 'JSON 格式不正确:' + e.message; + return; + } + this.designer.body.raw = text; + this.importJsonDialogVisible = false; + this.importJsonText = ''; + this.importJsonError = ''; + }, + }, computed: { designer: { get() { @@ -84,9 +149,60 @@ export default { diff --git a/yack-web/src/views/iot/platformConnect/tenantChannel/index.vue b/yack-web/src/views/iot/platformConnect/tenantChannel/index.vue index de11ab14..c8f6f42f 100644 --- a/yack-web/src/views/iot/platformConnect/tenantChannel/index.vue +++ b/yack-web/src/views/iot/platformConnect/tenantChannel/index.vue @@ -1,8 +1,16 @@ + + diff --git a/yack-web/src/views/iot/platformConnect/utils/requestTemplate.js b/yack-web/src/views/iot/platformConnect/utils/requestTemplate.js index dc91b9b0..b99767f9 100644 --- a/yack-web/src/views/iot/platformConnect/utils/requestTemplate.js +++ b/yack-web/src/views/iot/platformConnect/utils/requestTemplate.js @@ -1,3 +1,50 @@ +/** 键值行 -> Postman 风格批量文本(每行 key:value) */ +export function rowsToBulkText(rows) { + return (rows || []) + .map(item => { + if (!item || !item.key) { + return ''; + } + const prefix = item.enabled === false ? '#' : ''; + const value = item.value == null ? '' : String(item.value); + return `${prefix}${item.key}:${value}`; + }) + .filter(Boolean) + .join('\n'); +} + +/** 批量文本 -> 键值行;# 开头表示禁用 */ +export function bulkTextToRows(text) { + const rows = []; + (text || '').split(/\r?\n/).forEach(line => { + const trimmed = line.trim(); + if (!trimmed) { + return; + } + let enabled = true; + let content = trimmed; + if (content.startsWith('#')) { + enabled = false; + content = content.slice(1).trim(); + if (!content) { + return; + } + } + const colonIndex = content.indexOf(':'); + if (colonIndex < 0) { + rows.push({ enabled, key: content, value: '', description: '' }); + return; + } + rows.push({ + enabled, + key: content.slice(0, colonIndex).trim(), + value: content.slice(colonIndex + 1), + description: '', + }); + }); + return rows; +} + export function rowsToObject(rows) { const result = {} ;(rows || []).forEach(item => { @@ -76,7 +123,7 @@ export function serializeDesigner(value) { } else if (body.mode === 'x-www-form-urlencoded') { bodyTemplate = JSON.stringify({ mode: body.mode, urlencoded: body.urlencoded || [] }); } else { - bodyTemplate = ''; + bodyTemplate = null; } return { method: value.method,