diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/Device.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/Device.java index bdf3136..ece9db0 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/Device.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/Device.java @@ -243,5 +243,10 @@ public class Device extends TenantEntity { */ private Integer valueBattery; + /** + * 最后上数时间 + */ + private Date lastReportTime; + } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/DeviceBo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/DeviceBo.java index af177aa..aec419b 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/DeviceBo.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/DeviceBo.java @@ -270,5 +270,10 @@ public class DeviceBo extends BaseEntity { */ private Long batteryWarnLower; + /** + * 最后上数时间 + */ + private Date lastReportTime; + } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java index 1378e93..5405e29 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/DeviceVo.java @@ -312,6 +312,12 @@ public class DeviceVo implements Serializable { @ExcelProperty(value = "电量值") private Integer valueBattery; + /** + * 最后上数时间 + */ + @ExcelProperty(value = "最后上数时间") + private Date lastReportTime; + /** * 手机号 */ diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java index a28f970..b99ea3b 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java @@ -1,9 +1,11 @@ package com.intc.iot.controller; +import cn.dev33.satoken.annotation.SaCheckPermission; import com.intc.common.core.domain.R; import com.intc.common.mybatis.core.page.PageQuery; import com.intc.common.mybatis.core.page.TableDataInfo; import com.intc.common.satoken.utils.LoginHelper; +import com.intc.common.tenant.helper.TenantHelper; import com.intc.common.web.core.BaseController; import com.intc.common.core.config.properties.DeviceTypeProperties; import com.intc.fishery.domain.Device; @@ -46,6 +48,7 @@ import com.intc.iot.utils.ControllerHelper; import com.intc.iot.service.IotCloudService; import com.intc.iot.constant.IOTPropertyName; import com.intc.fishery.utils.MessageOpRecordUtil; +import com.intc.tdengine.service.IDeviceSensorDataService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -57,6 +60,9 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Date; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; @@ -72,6 +78,8 @@ import java.util.Map; @Tag(name = "生活物联网平台管理", description = "阿里云飞燕平台对接接口") public class IotController extends BaseController { + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final DeviceTypeProperties deviceTypeProperties; @Autowired(required = false) @@ -101,6 +109,9 @@ public class IotController extends BaseController { @Autowired(required = false) private DeviceDataHandler deviceDataHandler; + @Autowired(required = false) + private IDeviceSensorDataService deviceSensorDataService; + @Autowired(required = false) private javax.jms.Connection amqpConnection; @@ -191,6 +202,82 @@ public class IotController extends BaseController { } } + @Operation(summary = "回填设备最后上数时间") + @SaCheckPermission("fishery:device:edit") + @PostMapping("/device/backfill-last-report-time") + public R> backfillLastReportTime( + @RequestParam(defaultValue = "false") boolean ignoreTenant) { + if (deviceMapper == null || deviceSensorDataService == null) { + return R.fail("设备或TDengine服务未启用"); + } + + Map result = ignoreTenant + ? TenantHelper.ignore((java.util.function.Supplier>) this::doBackfillLastReportTime) + : doBackfillLastReportTime(); + result.put("ignoreTenant", ignoreTenant); + return R.ok(result); + } + + private Map doBackfillLastReportTime() { + int total = 0; + int updated = 0; + int skipped = 0; + int failed = 0; + + List devices = deviceMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .isNotNull(Device::getSerialNum) + .ne(Device::getSerialNum, "") + .select(Device::getId, Device::getSerialNum) + ); + + total = devices.size(); + for (Device device : devices) { + try { + String latestTime = deviceSensorDataService.getLatestReportTime(device.getSerialNum()); + if (latestTime == null || latestTime.isBlank()) { + skipped++; + continue; + } + + Device updateDevice = new Device(); + updateDevice.setId(device.getId()); + updateDevice.setLastReportTime(parseReportTime(latestTime)); + if (deviceMapper.updateById(updateDevice) > 0) { + updated++; + } else { + skipped++; + } + } catch (Exception e) { + failed++; + log.warn("回填设备最后上数时间失败: deviceId={}, serialNum={}, error={}", + device.getId(), device.getSerialNum(), e.getMessage()); + } + } + + Map result = new java.util.HashMap<>(); + result.put("total", total); + result.put("updated", updated); + result.put("skipped", skipped); + result.put("failed", failed); + return result; + } + + private Date parseReportTime(String time) { + String normalized = normalizeReportTime(time); + return Date.from(LocalDateTime.parse(normalized, DATETIME_FORMATTER) + .atZone(ZoneId.systemDefault()) + .toInstant()); + } + + private String normalizeReportTime(String time) { + String normalized = time.trim(); + if (normalized.length() > 19) { + normalized = normalized.substring(0, 19); + } + return normalized.replace('T', ' '); + } + @Operation(summary = "根据 ProductKey 和 DeviceName 查询设备信息") @GetMapping("/device/find") public R> findDeviceByProductKeyAndName( diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java index f4b1095..5d5f09b 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java @@ -40,7 +40,9 @@ import org.springframework.stereotype.Component; import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -2595,6 +2597,7 @@ public class DeviceDataHandler { if (sensorData.getBattery() != null) { updateDevice.setValueBattery(sensorData.getBattery().intValue()); } + updateDevice.setLastReportTime(parseSensorDataTime(sensorData)); int updated = deviceMapper.updateById(updateDevice); if (updated > 0) { log.debug("设备实时数据已更新: {}", deviceName); @@ -2604,6 +2607,29 @@ public class DeviceDataHandler { } } + private Date parseSensorDataTime(DeviceSensorData sensorData) { + String time = StrUtil.blankToDefault(sensorData.getCreateTime(), sensorData.getTime()); + if (StrUtil.isBlank(time)) { + return new Date(); + } + try { + return Date.from(LocalDateTime.parse(normalizeSensorDataTime(time), DATETIME_FORMATTER) + .atZone(ZoneId.systemDefault()) + .toInstant()); + } catch (Exception e) { + log.warn("解析传感器上数时间失败,使用当前服务器时间: {}", time); + return new Date(); + } + } + + private String normalizeSensorDataTime(String time) { + String normalized = time.trim(); + if (normalized.length() > 19) { + normalized = normalized.substring(0, 19); + } + return normalized.replace('T', ' '); + } + /** * 溶解氧联动控制(参考 C# SetDeviceSwitchLinkedOpen 逻辑) *
    diff --git a/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/mapper/DeviceSensorDataMapper.java b/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/mapper/DeviceSensorDataMapper.java index 90034b5..f4c3a48 100644 --- a/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/mapper/DeviceSensorDataMapper.java +++ b/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/mapper/DeviceSensorDataMapper.java @@ -77,4 +77,14 @@ public interface DeviceSensorDataMapper { @Param("endTime") String endTime, @Param("intervalType") Integer intervalType); + /** + * 查询设备最新上数时间 + * + * @param serialNum 设备序列号 + * @return 最新传感器数据 + */ + @DS("taos") + @InterceptorIgnore(tenantLine = "true") + DeviceSensorData getLatestReportTime(@Param("serialNum") String serialNum); + } diff --git a/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/IDeviceSensorDataService.java b/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/IDeviceSensorDataService.java index 18976c9..3fc2210 100644 --- a/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/IDeviceSensorDataService.java +++ b/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/IDeviceSensorDataService.java @@ -38,4 +38,12 @@ public interface IDeviceSensorDataService { * @return 设备传感器数据列表 */ public List getHistoryDataList(String serialNum, String startTime, String endTime, Integer intervalType); + + /** + * 查询设备最新上数时间 + * + * @param serialNum 设备序列号 + * @return 最新上数时间字符串,格式 yyyy-MM-dd HH:mm:ss;无数据时返回 null + */ + String getLatestReportTime(String serialNum); } diff --git a/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/impl/DeviceSensorDataService.java b/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/impl/DeviceSensorDataService.java index 4845c17..fb7cdb8 100644 --- a/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/impl/DeviceSensorDataService.java +++ b/intc-modules/intc-tdengine/src/main/java/com/intc/tdengine/service/impl/DeviceSensorDataService.java @@ -256,6 +256,30 @@ public class DeviceSensorDataService implements IDeviceSensorDataService { } return list; } + + /** + * 查询设备最新上数时间 + */ + @Override + public String getLatestReportTime(String serialNum) { + if (serialNum == null || serialNum.isBlank()) { + return null; + } + try { + Integer exists = deviceSensorDataMapper.checkTableExists(serialNum); + if (exists == null || exists == 0) { + return null; + } + DeviceSensorData latest = deviceSensorDataMapper.getLatestReportTime(serialNum); + if (latest == null) { + return null; + } + return latest.getCreateTime() != null ? latest.getCreateTime() : latest.getTime(); + } catch (Exception e) { + log.warn("查询设备最新上数时间失败: serialNum={}, error={}", serialNum, e.getMessage()); + return null; + } + } /** * 获取间隔类型描述 diff --git a/intc-modules/intc-tdengine/src/main/resources/mapper/tdengine/DeviceSensorDataMapper.xml b/intc-modules/intc-tdengine/src/main/resources/mapper/tdengine/DeviceSensorDataMapper.xml index aa54394..c2a811f 100644 --- a/intc-modules/intc-tdengine/src/main/resources/mapper/tdengine/DeviceSensorDataMapper.xml +++ b/intc-modules/intc-tdengine/src/main/resources/mapper/tdengine/DeviceSensorDataMapper.xml @@ -111,4 +111,13 @@ + + diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/controller/WeixinController.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/controller/WeixinController.java index 66d3360..b55688b 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/controller/WeixinController.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/controller/WeixinController.java @@ -262,6 +262,7 @@ public class WeixinController extends BaseController { // 9. 构建返回结果 WxPayOrderVo result = new WxPayOrderVo(); result.setPrepayId(prepayId); + result.setOutTradeNumber(order.getOutTradeNumber()); result.setTotalAmount(order.getTotalAmount()); result.setTimestamp(payParams.get("timeStamp")); result.setNonceStr(payParams.get("nonceStr")); @@ -278,6 +279,27 @@ public class WeixinController extends BaseController { } } + @Operation(summary = "主动确认微信支付订单") + @PostMapping("/pay/confirm_order") + public R confirmOrder(@RequestBody Map request) { + try { + if (payOrderBusinessService == null) { + return R.fail("支付服务未启用"); + } + + String outTradeNumber = request != null ? request.get("outTradeNumber") : null; + if (outTradeNumber == null || outTradeNumber.isEmpty()) { + return R.fail("商户订单号不能为空"); + } + + boolean success = payOrderBusinessService.confirmPaidOrder(outTradeNumber); + return success ? R.ok(true) : R.fail("订单未支付成功或确认失败"); + } catch (Exception e) { + log.error("确认支付订单失败", e); + return R.fail("确认支付订单失败: " + e.getMessage()); + } + } + @Operation(summary = "微信支付回调通知") @PostMapping("/pay_notify") public Map payNotify( diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/domain/vo/WxPayOrderVo.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/domain/vo/WxPayOrderVo.java index dcc903c..c836548 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/domain/vo/WxPayOrderVo.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/domain/vo/WxPayOrderVo.java @@ -24,6 +24,12 @@ public class WxPayOrderVo implements Serializable { @Schema(description = "预支付ID") private String prepayId; + /** + * 商户系统内部订单号 + */ + @Schema(description = "商户订单号") + private String outTradeNumber; + /** * 签名类型 */ diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java index f45e3b2..6436e74 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/PayOrderBusinessService.java @@ -92,6 +92,14 @@ public interface PayOrderBusinessService { */ int compensateUnpaidOrders(); + /** + * 主动确认单笔订单支付状态。 + * + * @param outTradeNumber 商户订单号 + * @return 是否已确认并处理支付成功 + */ + boolean confirmPaidOrder(String outTradeNumber); + /** * 修复已支付订单中设备有效期未落库的异常数据。 * diff --git a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java index a81ed75..c1cab16 100644 --- a/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java +++ b/intc-modules/intc-weixin/src/main/java/com/intc/weixin/service/impl/PayOrderBusinessServiceImpl.java @@ -570,6 +570,49 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService { return Boolean.TRUE.equals(success); } + @Override + public boolean confirmPaidOrder(String outTradeNumber) { + if (outTradeNumber == null || outTradeNumber.isEmpty()) { + return false; + } + + PayOrder order = queryByOutTradeNumber(outTradeNumber); + if (order == null) { + log.warn("[支付确认] 本地订单不存在: outTradeNumber={}", outTradeNumber); + return false; + } + if (PayOrderStatus.SUCCESS.equals(order.getOrderStatus())) { + log.info("[支付确认] 订单已成功: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber); + return true; + } + + String queryResult = wxPayService.queryOrderByOutTradeNo(outTradeNumber); + if (queryResult == null) { + log.warn("[支付确认] 查询微信订单失败: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber); + return false; + } + + JSONObject resultJson = JSONUtil.parseObj(queryResult); + String tradeState = resultJson.getStr("trade_state"); + if ("SUCCESS".equalsIgnoreCase(tradeState)) { + return compensateSingleOrder(order, resultJson); + } + + if ("CLOSED".equalsIgnoreCase(tradeState)) { + payOrderMapper.update(null, + new LambdaUpdateWrapper() + .eq(PayOrder::getId, order.getId()) + .ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS) + .set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED) + .set(PayOrder::getTradeState, "CLOSED") + .set(PayOrder::getTradeStateDescription, "微信侧已关闭") + ); + } + log.info("[支付确认] 订单未支付成功: orderId={}, outTradeNumber={}, tradeState={}", + order.getId(), outTradeNumber, tradeState); + return false; + } + @Override @Transactional(rollbackFor = Exception.class) public int repairPaidOrderDeviceDeadTime() {