yanderong
2026-07-25 42d613ae435438b5fdf8195bf230f2ae165781b6
feat: 增加光伏统计、光伏电站管理、逆变器管理、统计报表功能
31个文件已添加
2个文件已修改
2236 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TStationCollect.java 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TStationEarn.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TStationInfo.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationEarnController.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TStationCollectMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TStationEarnMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TStationInfoMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationEarnOverviewQuery.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationEarnQuery.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationEarnStatisticsQuery.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationInfoQuery.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java 337 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/KsolarUtils.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnExportVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnOverviewVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnStatisticsItemVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnStatisticsVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationInfoStatusVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TStationCollectMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TStationEarnMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TStationInfoMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/sql/t_station_collect.sql 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/sql/t_station_earn.sql 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/sql/t_station_info.sql 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TStationCollect.java
New file
@@ -0,0 +1,134 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * 电站逆变器实体。
 */
@Data
@TableName("t_station_collect")
@ApiModel(value = "TStationCollect对象", description = "电站逆变器")
public class TStationCollect implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键ID")
    private Long id;
    @TableField("platform_id")
    @ApiModelProperty("所属平台ID(0科士达、1华为、2阳光、3古瑞瓦特)")
    private Long platformId;
    @TableField("station_id")
    @ApiModelProperty("电站ID")
    private String stationId;
    @TableField("station_name")
    @ApiModelProperty("电站名称")
    private String stationName;
    @TableField("collect_id")
    @ApiModelProperty("逆变器ID")
    private String collectId;
    @TableField("collect_name")
    @ApiModelProperty("逆变器名称")
    private String collectName;
    @TableField("device_model")
    @ApiModelProperty("机型型号")
    private String deviceModel;
    @TableField("inverter_id")
    @ApiModelProperty("逆变器SN")
    private String inverterId;
    @TableField("installed_capacity")
    @ApiModelProperty("装机容量(kW)")
    private String installedCapacity;
    @TableField("power_num")
    @ApiModelProperty("装机容量数量")
    private Integer powerNum;
    @TableField("province_name")
    @ApiModelProperty("省")
    private String provinceName;
    @TableField("city_name")
    @ApiModelProperty("市")
    private String cityName;
    @TableField("area_name")
    @ApiModelProperty("区/县")
    private String areaName;
    @TableField("address")
    @ApiModelProperty("详细地址")
    private String address;
    @TableField("latitude")
    @ApiModelProperty("纬度")
    private String latitude;
    @TableField("longitude")
    @ApiModelProperty("经度")
    private String longitude;
    @TableField("status")
    @ApiModelProperty("上网状态(1在线、2离线、3故障)")
    private String status;
    @TableField("data_source")
    @ApiModelProperty("数据来源(1系统创建、2第三方接口)")
    private Integer dataSource;
    @TableField("sort_order")
    @ApiModelProperty("显示顺序")
    private Integer sortOrder;
    @TableField("create_by")
    @ApiModelProperty("创建者")
    private String createBy;
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("创建时间")
    private LocalDateTime createTime;
    @TableField("sync_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("同步时间")
    private LocalDateTime syncTime;
    @TableField("update_by")
    @ApiModelProperty("更新者")
    private String updateBy;
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("更新时间")
    private LocalDateTime updateTime;
    @TableField("remark")
    @ApiModelProperty("备注")
    private String remark;
    @TableField("del_flag")
    @TableLogic(value = "0", delval = "2")
    @ApiModelProperty("删除标志(0存在、2删除)")
    private String delFlag;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TStationEarn.java
New file
@@ -0,0 +1,88 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * 电站发电量统计实体。
 */
@Data
@TableName("t_station_earn")
@ApiModel(value = "TStationEarn对象", description = "电站发电量统计")
public class TStationEarn implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键ID")
    private Long id;
    @TableField("platform_id")
    @ApiModelProperty("平台ID,0表示科士达")
    private Long platformId;
    @TableField("station_id")
    @ApiModelProperty("电站ID")
    private String stationId;
    @TableField("station_name")
    @ApiModelProperty("电站名称")
    private String stationName;
    @TableField("save_time")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @ApiModelProperty("统计时间")
    private LocalDate saveTime;
    @TableField("elec_total")
    @ApiModelProperty("总发电量")
    private BigDecimal elecTotal;
    @TableField("earn_total")
    @ApiModelProperty("总收益")
    private BigDecimal earnTotal;
    @TableField("create_by")
    @ApiModelProperty("创建者")
    private String createBy;
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("创建时间")
    private LocalDateTime createTime;
    @TableField("update_by")
    @ApiModelProperty("更新者")
    private String updateBy;
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("更新时间")
    private LocalDateTime updateTime;
    @TableField("remark")
    @ApiModelProperty("备注")
    private String remark;
    @TableField("del_flag")
    @TableLogic(value = "0", delval = "2")
    @ApiModelProperty("删除标志(0存在、2删除)")
    private String delFlag;
    @TableField("data_source")
    @ApiModelProperty("数据来源(1系统创建、2第三方接口)")
    private Integer dataSource;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TStationInfo.java
New file
@@ -0,0 +1,160 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * 电站信息实体。
 */
@Data
@TableName("t_station_info")
@ApiModel(value = "TStationInfo对象", description = "电站信息")
public class TStationInfo implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键ID")
    private Long id;
    @TableField("platform_id")
    @ApiModelProperty("所属平台ID(0科士达、1华为、2阳光、3古瑞瓦特)")
    private Long platformId;
    @TableField("station_id")
    @ApiModelProperty("电站ID")
    private String stationId;
    @TableField("station_name")
    @ApiModelProperty("电站名称")
    private String stationName;
    @TableField("station_image")
    @ApiModelProperty("电站图片地址")
    private String stationImage;
    @TableField("station_type")
    @ApiModelProperty("电站类型(1光伏、2储能)")
    private String stationType;
    @TableField("run_status")
    @ApiModelProperty("运行状态(0正常/在线、1离线、2告警)")
    private String runStatus;
    @TableField("installed_capacity")
    @ApiModelProperty("装机容量(kW)")
    private BigDecimal installedCapacity;
    @TableField("realtime_power")
    @ApiModelProperty("实时功率(W)")
    private BigDecimal realtimePower;
    @TableField("device_count")
    @ApiModelProperty("设备总数")
    private Integer deviceCount;
    @TableField("daily_generation")
    @ApiModelProperty("日发电量(度/kWh)")
    private BigDecimal dailyGeneration;
    @TableField("grid_status")
    @ApiModelProperty("上网状态(0未上网、1已上网)")
    private String gridStatus;
    @TableField("data_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("指标数据时间")
    private LocalDateTime dataTime;
    @TableField("elec_price")
    @ApiModelProperty("电价")
    private String elecPrice;
    @TableField("contact_name")
    @ApiModelProperty("联系人")
    private String contactName;
    @TableField("phone")
    @ApiModelProperty("电话")
    private String phone;
    @TableField("province_name")
    @ApiModelProperty("省")
    private String provinceName;
    @TableField("city_name")
    @ApiModelProperty("市")
    private String cityName;
    @TableField("area_name")
    @ApiModelProperty("区/县")
    private String areaName;
    @TableField("address")
    @ApiModelProperty("详细地址")
    private String address;
    @TableField("latitude")
    @ApiModelProperty("纬度")
    private String latitude;
    @TableField("longitude")
    @ApiModelProperty("经度")
    private String longitude;
    @TableField("loan_status")
    @ApiModelProperty("并网状态(1未并网、2并网中、3已并网)")
    private Integer loanStatus;
    @TableField("data_source")
    @ApiModelProperty("数据来源(1系统创建、2第三方接口)")
    private Integer dataSource;
    @TableField("sort_order")
    @ApiModelProperty("显示顺序")
    private Integer sortOrder;
    @TableField("create_by")
    @ApiModelProperty("创建者")
    private String createBy;
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("创建时间")
    private LocalDateTime createTime;
    @TableField("sync_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("同步时间")
    private LocalDateTime syncTime;
    @TableField("update_by")
    @ApiModelProperty("更新者")
    private String updateBy;
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty("更新时间")
    private LocalDateTime updateTime;
    @TableField("remark")
    @ApiModelProperty("备注")
    private String remark;
    @TableField("del_flag")
    @TableLogic(value = "0", delval = "2")
    @ApiModelProperty("删除标志(0存在、2删除)")
    private String delFlag;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java
New file
@@ -0,0 +1,62 @@
package com.ruoyi.other.controller;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.other.api.domain.TStationCollect;
import com.ruoyi.other.query.StationCollectQuery;
import com.ruoyi.other.service.TStationCollectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Arrays;
@Api(tags = "管理后台-电站逆变器")
@RestController
@RequestMapping("/station-collect")
public class TStationCollectController {
    @Resource private TStationCollectService stationCollectService;
    @RequiresPermissions(value = {"/stationCollect/list"}, logical = Logical.OR)
    @GetMapping("/pageList") @ApiOperation("分页查询逆变器")
    public AjaxResult<PageInfo<TStationCollect>> pageList(StationCollectQuery query) {
        return AjaxResult.success(stationCollectService.pageList(query));
    }
    @RequiresPermissions(value = {"/stationCollect/add"}, logical = Logical.OR)
    @PostMapping("/add") @ApiOperation("添加逆变器")
    @Log(title = "【电站逆变器】新增", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody TStationCollect stationCollect) {
        return stationCollectService.addStationCollect(stationCollect)
                ? AjaxResult.success() : AjaxResult.error("新增失败");
    }
    @RequiresPermissions(value = {"/stationCollect/update"}, logical = Logical.OR)
    @PutMapping("/edit") @ApiOperation("编辑逆变器")
    @Log(title = "【电站逆变器】编辑", businessType = BusinessType.UPDATE)
    public AjaxResult edit(@RequestBody TStationCollect stationCollect) {
        return stationCollectService.updateStationCollect(stationCollect)
                ? AjaxResult.success() : AjaxResult.error("编辑失败");
    }
    @RequiresPermissions(value = {"/stationCollect/del"}, logical = Logical.OR)
    @DeleteMapping("/delete") @ApiOperation("删除逆变器")
    @Log(title = "【电站逆变器】删除", businessType = BusinessType.DELETE)
    public AjaxResult delete(@RequestBody Long[] ids) {
        if (ids == null || ids.length == 0) return AjaxResult.error("请选择需要删除的数据");
        return stationCollectService.removeByIds(Arrays.asList(ids))
                ? AjaxResult.success() : AjaxResult.error("删除失败");
    }
    @RequiresPermissions(value = {"/stationCollect/detail"}, logical = Logical.OR)
    @GetMapping("/detail/{id}") @ApiOperation("查询逆变器详情")
    public AjaxResult<TStationCollect> detail(@PathVariable Long id) {
        TStationCollect stationCollect = stationCollectService.getStationCollectDetail(id);
        return stationCollect == null ? AjaxResult.error("逆变器记录不存在") : AjaxResult.success(stationCollect);
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationEarnController.java
New file
@@ -0,0 +1,87 @@
package com.ruoyi.other.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import com.ruoyi.common.core.utils.WebUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.other.api.domain.TStationEarn;
import com.ruoyi.other.query.StationEarnQuery;
import com.ruoyi.other.query.StationEarnOverviewQuery;
import com.ruoyi.other.query.StationEarnStatisticsQuery;
import com.ruoyi.other.service.TStationEarnService;
import com.ruoyi.other.vo.StationEarnExportVO;
import com.ruoyi.other.vo.StationEarnOverviewVO;
import com.ruoyi.other.vo.StationEarnStatisticsVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
@Api(tags = "管理后台-电站收益统计")
@RestController
@RequestMapping("/station-earn")
public class TStationEarnController {
    @Resource private TStationEarnService stationEarnService;
    @RequiresPermissions(value = {"/stationEarn/list"}, logical = Logical.OR)
    @GetMapping("/pageList") @ApiOperation("分页查询电站收益")
    public AjaxResult<PageInfo<TStationEarn>> pageList(StationEarnQuery query) {
        return AjaxResult.success(stationEarnService.pageList(query));
    }
    @RequiresPermissions(value = {"/stationEarn/list"}, logical = Logical.OR)
    @GetMapping("/statistics")
    @ApiOperation("前端发电量分析统计")
    public AjaxResult<StationEarnStatisticsVO> statistics(StationEarnStatisticsQuery query) {
        return AjaxResult.success(stationEarnService.statistics(query));
    }
    @RequiresPermissions(value = {"/stationEarn/list"}, logical = Logical.OR)
    @GetMapping("/generationOverview")
    @ApiOperation("今日、本月和本年发电量概览")
    public AjaxResult<StationEarnOverviewVO> generationOverview(StationEarnOverviewQuery query) {
        return AjaxResult.success(stationEarnService.generationOverview(query));
    }
    @RequiresPermissions(value = {"/stationEarn/export"}, logical = Logical.OR)
    @GetMapping("/export") @ApiOperation("导出电站收益")
    @Log(title = "【电站收益统计】导出", businessType = BusinessType.EXPORT)
    public void export(StationEarnQuery query) throws Exception {
        List<TStationEarn> stationEarnList = stationEarnService.exportList(query == null ? new StationEarnQuery() : query);
        List<StationEarnExportVO> exportList = new ArrayList<>(stationEarnList.size());
        for (TStationEarn stationEarn : stationEarnList) {
            StationEarnExportVO exportVO = new StationEarnExportVO();
            BeanUtils.copyProperties(stationEarn, exportVO);
            exportVO.buildPlatformName(stationEarn.getPlatformId());
            exportVO.setSaveTime(stationEarn.getSaveTime() == null ? "" : stationEarn.getSaveTime().toString());
            exportList.add(exportVO);
        }
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), StationEarnExportVO.class, exportList);
        HttpServletResponse response = WebUtils.response();
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Disposition", "attachment;filename="
                + URLEncoder.encode("电站收益统计.xls", "UTF-8"));
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            workbook.write(outputStream);
            outputStream.flush();
        } finally {
            workbook.close();
        }
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java
New file
@@ -0,0 +1,67 @@
package com.ruoyi.other.controller;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.other.api.domain.TStationInfo;
import com.ruoyi.other.query.StationInfoQuery;
import com.ruoyi.other.service.TStationInfoService;
import com.ruoyi.other.vo.StationInfoStatusVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Arrays;
@Api(tags = "管理后台-电站信息")
@RestController
@RequestMapping("/station-info")
public class TStationInfoController {
    @Resource private TStationInfoService stationInfoService;
    @GetMapping("/pageList") @ApiOperation("分页查询电站信息")
    public AjaxResult<PageInfo<TStationInfo>> pageList(StationInfoQuery query) {
        return AjaxResult.success(stationInfoService.pageList(query));
    }
    @RequiresPermissions(value = {"/stationInfo/add"}, logical = Logical.OR)
    @PostMapping("/add") @ApiOperation("新增电站信息")
    @Log(title = "【电站信息】新增", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody TStationInfo info) {
        return stationInfoService.addStationInfo(info) ? AjaxResult.success() : AjaxResult.error("新增失败");
    }
    @RequiresPermissions(value = {"/stationInfo/update"}, logical = Logical.OR)
    @PutMapping("/edit") @ApiOperation("编辑电站信息")
    @Log(title = "【电站信息】编辑", businessType = BusinessType.UPDATE)
    public AjaxResult edit(@RequestBody TStationInfo info) {
        return stationInfoService.updateStationInfo(info) ? AjaxResult.success() : AjaxResult.error("编辑失败");
    }
    @RequiresPermissions(value = {"/stationInfo/del"}, logical = Logical.OR)
    @DeleteMapping("/delete") @ApiOperation("删除电站信息")
    @Log(title = "【电站信息】删除", businessType = BusinessType.DELETE)
    public AjaxResult delete(@RequestBody Long[] ids) {
        if (ids == null || ids.length == 0) return AjaxResult.error("请选择需要删除的数据");
        return stationInfoService.removeByIds(Arrays.asList(ids)) ? AjaxResult.success() : AjaxResult.error("删除失败");
    }
    /**
     * 调用第三方接口获取实时功率和电站状态
     */
    @GetMapping("/getStationStatus") @ApiOperation("获取电站状态")
    public AjaxResult<StationInfoStatusVO> getStationStatus() {
        return AjaxResult.success(stationInfoService.getStationStatus());
    }
    @PostMapping("/syncKsolarDataTask") @ApiOperation("手动同步数据")
    public void syncKsolarDataTask() {
        stationInfoService.syncKsolarDataTask();
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TStationCollectMapper.java
New file
@@ -0,0 +1,10 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.other.api.domain.TStationCollect;
/**
 * 电站逆变器 Mapper。
 */
public interface TStationCollectMapper extends BaseMapper<TStationCollect> {
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TStationEarnMapper.java
New file
@@ -0,0 +1,10 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.other.api.domain.TStationEarn;
/**
 * 电站发电量统计 Mapper。
 */
public interface TStationEarnMapper extends BaseMapper<TStationEarn> {
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TStationInfoMapper.java
New file
@@ -0,0 +1,10 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.other.api.domain.TStationInfo;
/**
 * 电站信息 Mapper。
 */
public interface TStationInfoMapper extends BaseMapper<TStationInfo> {
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java
New file
@@ -0,0 +1,17 @@
package com.ruoyi.other.query;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("电站逆变器分页查询参数")
public class StationCollectQuery extends BasePage {
    @ApiModelProperty("所属平台ID") private Long platformId;
    @ApiModelProperty("电站名称") private String stationName;
    @ApiModelProperty("逆变器名称") private String collectName;
    @ApiModelProperty("机型型号") private String deviceModel;
    @ApiModelProperty("逆变器SN") private String inverterId;
    @ApiModelProperty("状态") private String status;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationEarnOverviewQuery.java
New file
@@ -0,0 +1,16 @@
package com.ruoyi.other.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("电站发电量概览查询参数")
public class StationEarnOverviewQuery {
    @ApiModelProperty("所属平台ID,不传表示全部平台")
    private Long platformId;
    @ApiModelProperty("电站ID,不传表示全部电站")
    private String stationId;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationEarnQuery.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.other.query;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
@ApiModel("电站收益分页查询参数")
public class StationEarnQuery extends BasePage {
    @ApiModelProperty("所属平台ID") private Long platformId;
    @ApiModelProperty("电站ID") private String stationId;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty("统计开始日期,yyyy-MM-dd") private LocalDate startSaveTime;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty("统计结束日期,yyyy-MM-dd") private LocalDate endSaveTime;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationEarnStatisticsQuery.java
New file
@@ -0,0 +1,30 @@
package com.ruoyi.other.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
@ApiModel("电站发电量统计查询参数")
public class StationEarnStatisticsQuery {
    @ApiModelProperty("统计维度:day按天、month按月、year按年")
    private String dimension = "day";
    @ApiModelProperty("所属平台ID,不传表示全部平台")
    private Long platformId;
    @ApiModelProperty("电站ID,不传表示全部电站")
    private String stationId;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty("按天统计开始日期,yyyy-MM-dd")
    private LocalDate startSaveTime;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty("按天统计结束日期,yyyy-MM-dd")
    private LocalDate endSaveTime;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationInfoQuery.java
New file
@@ -0,0 +1,16 @@
package com.ruoyi.other.query;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("电站信息分页查询参数")
public class StationInfoQuery extends BasePage {
    @ApiModelProperty("所属平台ID") private Long platformId;
    @ApiModelProperty("电站名称") private String stationName;
    @ApiModelProperty("电站类型") private String stationType;
    @ApiModelProperty("运行状态") private String runStatus;
    @ApiModelProperty("上网状态") private String gridStatus;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java
New file
@@ -0,0 +1,23 @@
package com.ruoyi.other.service;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TStationCollect;
import com.ruoyi.other.query.StationCollectQuery;
/**
 * 电站逆变器服务。
 */
public interface TStationCollectService extends IService<TStationCollect> {
    boolean addStationCollect(TStationCollect stationCollect);
    boolean saveCollectBaseList(JSONArray collectBaseList);
    PageInfo<TStationCollect> pageList(StationCollectQuery query);
    boolean updateStationCollect(TStationCollect stationCollect);
    TStationCollect getStationCollectDetail(Long id);
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java
New file
@@ -0,0 +1,34 @@
package com.ruoyi.other.service;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TStationEarn;
import com.ruoyi.other.query.StationEarnQuery;
import com.ruoyi.other.query.StationEarnOverviewQuery;
import com.ruoyi.other.query.StationEarnStatisticsQuery;
import com.ruoyi.other.vo.StationEarnOverviewVO;
import com.ruoyi.other.vo.StationEarnStatisticsVO;
import java.time.LocalDate;
import java.util.List;
/**
 * 电站发电量统计服务。
 */
public interface TStationEarnService extends IService<TStationEarn> {
    boolean addStationEarn(TStationEarn stationEarn);
    boolean saveStationEarnList(JSONArray stationEarnList);
    LocalDate getMaxSaveTime();
    PageInfo<TStationEarn> pageList(StationEarnQuery query);
    List<TStationEarn> exportList(StationEarnQuery query);
    StationEarnStatisticsVO statistics(StationEarnStatisticsQuery query);
    StationEarnOverviewVO generationOverview(StationEarnOverviewQuery query);
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java
New file
@@ -0,0 +1,34 @@
package com.ruoyi.other.service;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.other.api.domain.TStationInfo;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.query.StationInfoQuery;
import com.ruoyi.other.vo.StationInfoStatusVO;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
 * 电站信息服务。
 */
public interface TStationInfoService extends IService<TStationInfo> {
    boolean addStationInfo(TStationInfo stationInfo);
    PageInfo<TStationInfo> pageList(StationInfoQuery query);
    boolean updateStationInfo(TStationInfo stationInfo);
    /**
     * 每天同步科士达数据到数据库
     */
    void syncKsolarDataTask();
    /**
     * 实时获取电站状态
     */
    StationInfoStatusVO getStationStatus();
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
New file
@@ -0,0 +1,280 @@
package com.ruoyi.other.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TStationCollect;
import com.ruoyi.other.api.domain.TStationInfo;
import com.ruoyi.other.mapper.TStationCollectMapper;
import com.ruoyi.other.mapper.TStationInfoMapper;
import com.ruoyi.other.query.StationCollectQuery;
import com.ruoyi.other.service.TStationCollectService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * 电站逆变器服务实现。
 */
@Service
public class TStationCollectServiceImpl
        extends ServiceImpl<TStationCollectMapper, TStationCollect>
        implements TStationCollectService {
    @Resource
    private TStationInfoMapper stationInfoMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addStationCollect(TStationCollect stationCollect) {
        validate(stationCollect);
        stationCollect.setCollectId("1"+System.currentTimeMillis());
        stationCollect.setDataSource(1);
        fillDefaults(stationCollect);
        stationCollect.setId(null);
        stationCollect.setCreateTime(LocalDateTime.now());
        stationCollect.setUpdateTime(LocalDateTime.now());
        return save(stationCollect);
    }
    @Override
    public PageInfo<TStationCollect> pageList(StationCollectQuery query) {
        if (query == null) query = new StationCollectQuery();
        PageInfo<TStationCollect> page = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        lambdaQuery()
                .eq(query.getPlatformId() != null, TStationCollect::getPlatformId, query.getPlatformId())
                .like(StringUtils.isNotBlank(query.getStationName()), TStationCollect::getStationName, query.getStationName())
                .like(StringUtils.isNotBlank(query.getCollectName()), TStationCollect::getCollectName, query.getCollectName())
                .like(StringUtils.isNotBlank(query.getDeviceModel()), TStationCollect::getDeviceModel, query.getDeviceModel())
                .like(StringUtils.isNotBlank(query.getInverterId()), TStationCollect::getInverterId, query.getInverterId())
                .eq(StringUtils.isNotBlank(query.getStatus()), TStationCollect::getStatus, query.getStatus())
                .orderByAsc(TStationCollect::getSortOrder).orderByDesc(TStationCollect::getCreateTime).page(page);
        fillStationAddress(page.getRecords());
        return page;
    }
    @Override
    public TStationCollect getStationCollectDetail(Long id) {
        if (id == null) {
            return null;
        }
        TStationCollect stationCollect = getById(id);
        if (stationCollect != null) {
            List<TStationCollect> stationCollectList = new ArrayList<>(1);
            stationCollectList.add(stationCollect);
            fillStationAddress(stationCollectList);
        }
        return stationCollect;
    }
    private void fillStationAddress(List<TStationCollect> stationCollectList) {
        if (stationCollectList == null || stationCollectList.isEmpty()) {
            return;
        }
        Set<String> stationIds = stationCollectList.stream()
                .map(TStationCollect::getStationId)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toSet());
        if (stationIds.isEmpty()) {
            return;
        }
        List<TStationInfo> stationInfoList = stationInfoMapper.selectList(
                new LambdaQueryWrapper<TStationInfo>()
                        .select(TStationInfo::getStationId,
                                TStationInfo::getProvinceName,
                                TStationInfo::getCityName,
                                TStationInfo::getAreaName,
                                TStationInfo::getAddress)
                        .in(TStationInfo::getStationId, stationIds));
        Map<String, TStationInfo> stationInfoMap = stationInfoList.stream()
                .filter(item -> StringUtils.isNotBlank(item.getStationId()))
                .collect(Collectors.toMap(TStationInfo::getStationId,
                        Function.identity(), (first, ignored) -> first));
        for (TStationCollect stationCollect : stationCollectList) {
            TStationInfo stationInfo = stationInfoMap.get(stationCollect.getStationId());
            if (stationInfo != null) {
                stationCollect.setProvinceName(stationInfo.getProvinceName());
                stationCollect.setCityName(stationInfo.getCityName());
                stationCollect.setAreaName(stationInfo.getAreaName());
                stationCollect.setAddress(stationInfo.getAddress());
            }
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateStationCollect(TStationCollect stationCollect) {
        if (stationCollect == null || stationCollect.getId() == null) {
            throw new ServiceException("逆变器记录ID不能为空");
        }
        if (getById(stationCollect.getId()) == null) {
            throw new ServiceException("逆变器记录不存在");
        }
        validate(stationCollect);
        if (lambdaQuery().eq(TStationCollect::getCollectId, stationCollect.getCollectId())
                .ne(TStationCollect::getId, stationCollect.getId()).count() > 0) {
            throw new ServiceException("逆变器ID已存在");
        }
        stationCollect.setCreateBy(null);
        stationCollect.setCreateTime(null);
        stationCollect.setDelFlag(null);
        stationCollect.setUpdateTime(LocalDateTime.now());
        return updateById(stationCollect);
    }
    /**
     * 将第三方逆变器基础数据转换为实体并批量保存。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveCollectBaseList(JSONArray collectBaseList) {
        if (collectBaseList == null || collectBaseList.isEmpty()) {
            return false;
        }
        Set<String> existingKeys = getExistingCollectKeys(); // 数据库中存在的所有逆变器ID
        List<TStationCollect> stationCollectList = new ArrayList<>(collectBaseList.size());
        for (Object item : collectBaseList) {
            JSONObject jsonObject = JSONObject.parseObject(item.toString());
            TStationCollect stationCollect = buildStationCollect(jsonObject);
            if (existingKeys.contains(stationCollect.getCollectId())) {
                continue;
            }
            stationCollectList.add(stationCollect);
        }
        return !stationCollectList.isEmpty() && saveBatch(stationCollectList);
    }
    private TStationCollect buildStationCollect(JSONObject jsonObject) {
        String stationId = jsonObject.getString("stationId");
        String stationName = jsonObject.getString("stationName");
        String collectName = jsonObject.getString("collectName");
        TStationCollect stationCollect = new TStationCollect();
        stationCollect.setPlatformId(0L);
        stationCollect.setStationId(stationId);
        stationCollect.setStationName(stationName);
        stationCollect.setCollectId(jsonObject.getString("collectId"));
        stationCollect.setCollectName(collectName);
        stationCollect.setDeviceModel(jsonObject.getString("deviceModel"));
        stationCollect.setInverterId(jsonObject.getString("inverterId"));
        stationCollect.setInstalledCapacity(jsonObject.getString("capacity"));
        stationCollect.setPowerNum(parsePowerNum(jsonObject.getString("powerNum")));
        stationCollect.setProvinceName(jsonObject.getString("province"));
        stationCollect.setCityName(jsonObject.getString("city"));
        stationCollect.setAreaName(jsonObject.getString("district"));
        stationCollect.setAddress(jsonObject.getString("address"));
        stationCollect.setLatitude(jsonObject.getString("lat"));
        stationCollect.setLongitude(jsonObject.getString("lon"));
        stationCollect.setDataSource(2);
        stationCollect.setSyncTime(LocalDateTime.now());
        stationCollect.setCreateTime(parseDateTime(jsonObject.getString("saveTime")));
        stationCollect.setUpdateTime(LocalDateTime.now());
        fillDefaults(stationCollect);
        return stationCollect;
    }
    private Set<String> getExistingCollectKeys() {
        List<TStationCollect> existingList = lambdaQuery().select(TStationCollect::getCollectId).list();
        Set<String> existingKeys = new HashSet<>();
        for (TStationCollect stationCollect : existingList) {
            existingKeys.add(stationCollect.getCollectId());
        }
        return existingKeys;
    }
    private void validate(TStationCollect stationCollect) {
        if (stationCollect == null) {
            throw new ServiceException("电站逆变器参数不能为空");
        }
        if (stationCollect.getStationId() == null) {
            throw new ServiceException("电站ID不能为空");
        }
        if (StringUtils.isBlank(stationCollect.getStationName())) {
            throw new ServiceException("电站名称不能为空");
        }
        if (StringUtils.isBlank(stationCollect.getCollectName())) {
            throw new ServiceException("逆变器名称不能为空");
        }
    }
    private void fillDefaults(TStationCollect stationCollect) {
        stationCollect.setPlatformId(stationCollect.getPlatformId() == null ? 0L : stationCollect.getPlatformId());
        stationCollect.setPowerNum(stationCollect.getPowerNum() == null ? 0 : stationCollect.getPowerNum());
        stationCollect.setStatus(StringUtils.isBlank(stationCollect.getStatus()) ? "0" : stationCollect.getStatus());
        stationCollect.setSortOrder(stationCollect.getSortOrder() == null ? 0 : stationCollect.getSortOrder());
        stationCollect.setCreateBy(StringUtils.defaultString(stationCollect.getCreateBy()));
        stationCollect.setUpdateBy(StringUtils.defaultString(stationCollect.getUpdateBy()));
        stationCollect.setDelFlag("0");
    }
    private Long parseLong(String value) {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        try {
            return Long.valueOf(value);
        } catch (NumberFormatException exception) {
            return null;
        }
    }
    private BigDecimal parseBigDecimal(String value) {
        if (StringUtils.isBlank(value)) {
            return BigDecimal.ZERO;
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException exception) {
            return BigDecimal.ZERO;
        }
    }
    private Integer parsePowerNum(String value) {
        if (StringUtils.isBlank(value)) {
            return 0;
        }
        String number = value.replaceAll("[^0-9.]", "");
        if (StringUtils.isBlank(number)) {
            return 0;
        }
        try {
            return new BigDecimal(number).intValue();
        } catch (NumberFormatException exception) {
            return 0;
        }
    }
    private LocalDateTime parseDateTime(String value) {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        } catch (DateTimeParseException exception) {
            return null;
        }
    }
    private String firstNotBlank(String preferred, String fallback) {
        return StringUtils.isNotBlank(preferred) ? preferred : fallback;
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java
New file
@@ -0,0 +1,335 @@
package com.ruoyi.other.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TStationEarn;
import com.ruoyi.other.mapper.TStationEarnMapper;
import com.ruoyi.other.query.StationEarnQuery;
import com.ruoyi.other.query.StationEarnOverviewQuery;
import com.ruoyi.other.query.StationEarnStatisticsQuery;
import com.ruoyi.other.service.TStationEarnService;
import com.ruoyi.other.vo.StationEarnStatisticsItemVO;
import com.ruoyi.other.vo.StationEarnStatisticsVO;
import com.ruoyi.other.vo.StationEarnOverviewVO;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * 电站发电量统计服务实现。
 */
@Service
public class TStationEarnServiceImpl
        extends ServiceImpl<TStationEarnMapper, TStationEarn>
        implements TStationEarnService {
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addStationEarn(TStationEarn stationEarn) {
        validate(stationEarn);
        stationEarn.setId(null);
        stationEarn.setCreateTime(LocalDateTime.now());
        stationEarn.setUpdateTime(LocalDateTime.now());
        return save(stationEarn);
    }
    /**
     * 将第三方电站收益数据转换为实体并批量保存。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveStationEarnList(JSONArray stationEarnList) {
        if (stationEarnList == null || stationEarnList.isEmpty()) {
            return false;
        }
        Set<String> existingKeys = getExistingEarnKeys();
        Set<String> pendingKeys = new HashSet<>();
        List<TStationEarn> earnList = new ArrayList<>(stationEarnList.size());
        for (Object item : stationEarnList) {
            JSONObject jsonObject = JSONObject.parseObject(item.toString());
            TStationEarn stationEarn = buildStationEarn(jsonObject);
            if (stationEarn == null) {
                continue;
            }
            String earnKey = buildEarnKey(stationEarn);
            if (existingKeys.contains(earnKey) || !pendingKeys.add(earnKey)) {
                continue;
            }
            earnList.add(stationEarn);
        }
        if(CollectionUtils.isNotEmpty(earnList)) {
            List<List<TStationEarn>> earnAllList = Lists.partition(earnList, 500);
            earnAllList.forEach(this::saveBatch);
        }
        return true;
    }
    /**
     * 查询发电量统计记录中的最大统计日期。
     *
     * @return 最大统计日期,无记录时返回 null
     */
    @Override
    public LocalDate getMaxSaveTime() {
        TStationEarn latest = lambdaQuery()
                .select(TStationEarn::getSaveTime)
                .isNotNull(TStationEarn::getSaveTime)
                .orderByDesc(TStationEarn::getSaveTime)
                .last("LIMIT 1")
                .one();
        return latest == null ? null : latest.getSaveTime();
    }
    @Override
    public PageInfo<TStationEarn> pageList(StationEarnQuery query) {
        if (query == null) query = new StationEarnQuery();
        PageInfo<TStationEarn> page = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        page(page, buildQueryWrapper(query));
        return page;
    }
    @Override
    public List<TStationEarn> exportList(StationEarnQuery query) {
        return list(buildQueryWrapper(query));
    }
    @Override
    public StationEarnStatisticsVO statistics(StationEarnStatisticsQuery query) {
        if (query == null) {
            query = new StationEarnStatisticsQuery();
        }
        String dimension = StringUtils.isBlank(query.getDimension()) ? "day" : query.getDimension().toLowerCase();
        LocalDate today = LocalDate.now();
        LocalDate startDate = null;
        LocalDate endDate = null;
        if ("day".equals(dimension)) {
            startDate = query.getStartSaveTime();
            endDate = query.getEndSaveTime();
            if (startDate == null || endDate == null) {
                throw new ServiceException("按天统计时开始日期和结束日期不能为空");
            }
            if (startDate.isAfter(endDate)) {
                throw new ServiceException("开始日期不能晚于结束日期");
            }
        } else if ("month".equals(dimension)) {
            startDate = today.withDayOfYear(1);
            endDate = today;
        } else if (!"year".equals(dimension)) {
            throw new ServiceException("统计维度仅支持day、month、year");
        }
        LambdaQueryWrapper<TStationEarn> wrapper = new LambdaQueryWrapper<TStationEarn>()
                .select(TStationEarn::getSaveTime, TStationEarn::getElecTotal, TStationEarn::getEarnTotal)
                .eq(query.getPlatformId() != null, TStationEarn::getPlatformId, query.getPlatformId())
                .eq(StringUtils.isNotBlank(query.getStationId()), TStationEarn::getStationId, query.getStationId())
                .ge(startDate != null, TStationEarn::getSaveTime, startDate)
                .le(endDate != null, TStationEarn::getSaveTime, endDate)
                .isNotNull(TStationEarn::getSaveTime)
                .orderByAsc(TStationEarn::getSaveTime);
        List<TStationEarn> records = list(wrapper);
        Map<String, BigDecimal[]> grouped = createStatisticsGroups(dimension, startDate, endDate);
        for (TStationEarn record : records) {
            String label = statisticsLabel(dimension, record.getSaveTime());
            BigDecimal[] values = grouped.computeIfAbsent(label,
                    key -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
            values[0] = values[0].add(defaultZero(record.getElecTotal()));
            values[1] = values[1].add(defaultZero(record.getEarnTotal()));
        }
        BigDecimal totalGeneration = BigDecimal.ZERO;
        BigDecimal totalEarn = BigDecimal.ZERO;
        List<StationEarnStatisticsItemVO> items = new ArrayList<>(grouped.size());
        for (Map.Entry<String, BigDecimal[]> entry : grouped.entrySet()) {
            totalGeneration = totalGeneration.add(entry.getValue()[0]);
            totalEarn = totalEarn.add(entry.getValue()[1]);
            items.add(new StationEarnStatisticsItemVO(entry.getKey(), entry.getValue()[0], entry.getValue()[1]));
        }
        StationEarnStatisticsVO result = new StationEarnStatisticsVO();
        result.setDimension(dimension);
        result.setTotalGeneration(totalGeneration);
        result.setTotalEarn(totalEarn);
        result.setItems(items);
        return result;
    }
    @Override
    public StationEarnOverviewVO generationOverview(StationEarnOverviewQuery query) {
        if (query == null) {
            query = new StationEarnOverviewQuery();
        }
        LocalDate today = LocalDate.now();
        LocalDate yearStart = today.withDayOfYear(1);
        LocalDate monthStart = today.withDayOfMonth(1);
        List<TStationEarn> records = list(new LambdaQueryWrapper<TStationEarn>()
                .select(TStationEarn::getSaveTime, TStationEarn::getElecTotal)
                .eq(query.getPlatformId() != null, TStationEarn::getPlatformId, query.getPlatformId())
                .eq(StringUtils.isNotBlank(query.getStationId()), TStationEarn::getStationId, query.getStationId())
                .between(TStationEarn::getSaveTime, yearStart, today));
        BigDecimal todayGeneration = BigDecimal.ZERO;
        BigDecimal monthGeneration = BigDecimal.ZERO;
        BigDecimal yearGeneration = BigDecimal.ZERO;
        for (TStationEarn record : records) {
            BigDecimal generation = defaultZero(record.getElecTotal());
            yearGeneration = yearGeneration.add(generation);
            if (!record.getSaveTime().isBefore(monthStart)) {
                monthGeneration = monthGeneration.add(generation);
            }
            if (today.equals(record.getSaveTime())) {
                todayGeneration = todayGeneration.add(generation);
            }
        }
        StationEarnOverviewVO result = new StationEarnOverviewVO();
        result.setTodayGeneration(todayGeneration);
        result.setMonthGeneration(monthGeneration);
        result.setYearGeneration(yearGeneration.divide(new BigDecimal("10000"), 4, RoundingMode.HALF_UP)
                .stripTrailingZeros());
        return result;
    }
    private Map<String, BigDecimal[]> createStatisticsGroups(String dimension, LocalDate startDate, LocalDate endDate) {
        Map<String, BigDecimal[]> groups = new LinkedHashMap<>();
        if ("day".equals(dimension)) {
            for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
                groups.put(date.toString(), new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
            }
        } else if ("month".equals(dimension)) {
            for (LocalDate date = startDate.withDayOfMonth(1); !date.isAfter(endDate); date = date.plusMonths(1)) {
                groups.put(date.getMonthValue() + "月", new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
            }
        }
        return groups;
    }
    private String statisticsLabel(String dimension, LocalDate saveTime) {
        if ("day".equals(dimension)) {
            return saveTime.toString();
        }
        if ("month".equals(dimension)) {
            return saveTime.getMonthValue() + "月";
        }
        return String.valueOf(saveTime.getYear());
    }
    private LambdaQueryWrapper<TStationEarn> buildQueryWrapper(StationEarnQuery query) {
        return new LambdaQueryWrapper<TStationEarn>()
                .eq(query.getPlatformId() != null, TStationEarn::getPlatformId, query.getPlatformId())
                .eq(StringUtils.isNotBlank(query.getStationId()), TStationEarn::getStationId, query.getStationId())
                .ge(query.getStartSaveTime() != null, TStationEarn::getSaveTime, query.getStartSaveTime())
                .le(query.getEndSaveTime() != null, TStationEarn::getSaveTime, query.getEndSaveTime())
                .orderByDesc(TStationEarn::getSaveTime)
                .orderByDesc(TStationEarn::getCreateTime);
    }
    private TStationEarn buildStationEarn(JSONObject jsonObject) {
        String stationId = jsonObject.getString("stationId");
        LocalDate saveTime = parseSaveTime(jsonObject.getString("saveTime"));
        if (StringUtils.isBlank(stationId) || saveTime == null) {
            return null;
        }
        LocalDateTime now = LocalDateTime.now();
        TStationEarn stationEarn = new TStationEarn();
        stationEarn.setPlatformId(0L);
        stationEarn.setStationId(stationId);
        stationEarn.setStationName(jsonObject.getString("stationName"));
        stationEarn.setSaveTime(saveTime);
        stationEarn.setEarnTotal(parseBigDecimal(jsonObject.getString("earnTotal")));
        stationEarn.setElecTotal(parseBigDecimal(jsonObject.getString("elecTotal")));
        stationEarn.setDataSource(2);
        stationEarn.setCreateBy("");
        stationEarn.setUpdateBy("");
        stationEarn.setCreateTime(now);
        stationEarn.setUpdateTime(now);
        stationEarn.setDelFlag("0");
        return stationEarn;
    }
    private Set<String> getExistingEarnKeys() {
        List<TStationEarn> existingList = lambdaQuery().select(TStationEarn::getStationId, TStationEarn::getSaveTime).list();
        Set<String> existingKeys = new HashSet<>();
        for (TStationEarn stationEarn : existingList) {
            if (StringUtils.isNotBlank(stationEarn.getStationId()) && stationEarn.getSaveTime() != null) {
                existingKeys.add(buildEarnKey(stationEarn));
            }
        }
        return existingKeys;
    }
    private String buildEarnKey(TStationEarn stationEarn) {
        return stationEarn.getStationId() + "|" + stationEarn.getSaveTime();
    }
    private void validate(TStationEarn stationEarn) {
        if (stationEarn == null) {
            throw new ServiceException("发电量统计参数不能为空");
        }
        if (stationEarn.getSaveTime() == null) {
            throw new ServiceException("统计时间不能为空");
        }
    }
    private BigDecimal defaultZero(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private BigDecimal parseBigDecimal(String value) {
        if (StringUtils.isBlank(value)) {
            return BigDecimal.ZERO;
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException exception) {
            return BigDecimal.ZERO;
        }
    }
    private LocalDate parseSaveTime(String value) {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        DateTimeFormatter[] formatters = {
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
        };
        for (DateTimeFormatter formatter : formatters) {
            try {
                return LocalDateTime.parse(value, formatter).toLocalDate();
            } catch (DateTimeParseException ignored) {
                // 尝试下一个时间格式
            }
        }
        try {
            return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        } catch (DateTimeParseException exception) {
            return null;
        }
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java
New file
@@ -0,0 +1,337 @@
package com.ruoyi.other.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TStationInfo;
import com.ruoyi.other.mapper.TStationInfoMapper;
import com.ruoyi.other.service.TStationCollectService;
import com.ruoyi.other.service.TStationEarnService;
import com.ruoyi.other.service.TStationInfoService;
import com.ruoyi.other.query.StationInfoQuery;
import com.ruoyi.other.util.KsolarUtils;
import com.ruoyi.other.vo.StationInfoStatusVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * 电站信息服务实现。
 */
@Service
@Slf4j
public class TStationInfoServiceImpl
        extends ServiceImpl<TStationInfoMapper, TStationInfo>
        implements TStationInfoService {
    @Autowired
    private TStationCollectService collectService;
    @Autowired
    private TStationEarnService stationEarnService;
    /**
     * 每天同步科士达数据到数据库
     */
    @Transactional(rollbackFor = Exception.class)
    public void syncKsolarDataTask() {
        String token = KsolarUtils.token();
        JSONObject statusJsonObj = KsolarUtils.getListStatus(token);
        log.info("-------1开始同步科士达-电站列表信息----");
        // 电站列表
        JSONArray listInfoJsonArray = KsolarUtils.getStationListInfo(1, 1000, token);
        // 电站基本信息列表
        JSONArray listBaseJsonArray = KsolarUtils.getStationListBase(1, 1000, token);
        List<TStationInfo> stationInfoList = build(listInfoJsonArray, listBaseJsonArray);
        if(CollectionUtils.isNotEmpty(stationInfoList)) {
            List<TStationInfo> existingStationList = lambdaQuery().select(TStationInfo::getStationId).list();
            Set<String> existingStationIds = new HashSet<>();
            for (TStationInfo stationInfo : existingStationList) {
                if (stationInfo.getStationId() != null) {
                    existingStationIds.add(stationInfo.getStationId());
                }
            }
            Set<String> pendingStationIds = new HashSet<>();
            stationInfoList.removeIf(stationInfo -> stationInfo.getStationId() == null || existingStationIds.contains(stationInfo.getStationId()) || !pendingStationIds.add(stationInfo.getStationId()));
            if (CollectionUtils.isNotEmpty(stationInfoList)) {
                saveBatch(stationInfoList);
            }
        }
        log.info("-------1结束同步科士达-电站列表信息----");
        log.info("-------2开始同步科士达-逆变器列表----");
        // 逆变器列表
        JSONArray collectBaseList = KsolarUtils.getCollectBaseList(1, 1000, token);
        collectService.saveCollectBaseList(collectBaseList);
        log.info("-------2结束同步科士达-逆变器列表----");
        log.info("-------3开始同步科士达-电站的电量和收益统计查询----");
        LocalDate startDate = stationEarnService.getMaxSaveTime();
        String startDateStr = "";
        String endDateStr = "";
        if(Objects.nonNull(startDate)) {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            // String dateStr = startDate.plusDays(1).format(formatter);
            String dateStr = startDate.format(formatter);
            startDateStr = dateStr + " 00:00:00";
            endDateStr = LocalDateTime.now().format(formatter);
        }
        JSONArray stationEarnList = KsolarUtils.getStationElecList(startDateStr, endDateStr, token);
        stationEarnService.saveStationEarnList(stationEarnList);
        log.info("-------3结束同步科士达-电站的电量和收益统计查询----");
    }
    /**
     * 根据电站列表信息和电站基础信息批量构建电站对象。
     * 两个数组通过 stationId 进行关联。
     */
    public List<TStationInfo> build(JSONArray listInfoJsonArray, JSONArray listBaseJsonArray) {
        if (listInfoJsonArray == null || listInfoJsonArray.isEmpty()) {
            log.error("电站列表信息获取第三方API数据为空!");
            return null;
        }
        Map<String, JSONObject> baseMap = buildBaseMap(listBaseJsonArray);
        List<TStationInfo> stationInfoList = new ArrayList<>(listInfoJsonArray.size());
        for (Object item : listInfoJsonArray) {
            JSONObject info = JSONObject.parseObject(item.toString());
            String stationId = info.getString("stationId");
            if (StringUtils.isBlank(stationId)) {
                log.error("电站列表信息中的stationId不能为空!");
                continue;
            }
            stationInfoList.add(buildStationInfo(info, baseMap.get(stationId)));
        }
        return stationInfoList;
    }
    private TStationInfo buildStationInfo(JSONObject info, JSONObject base) {
        String stationId = info.getString("stationId");
        TStationInfo stationInfo = new TStationInfo();
        stationInfo.setPlatformId(0L);
        stationInfo.setStationId(stationId);
        stationInfo.setStationName(info.getString("stationName"));
        stationInfo.setStationType(info.getString("stationType"));
        stationInfo.setRunStatus(info.getString("status"));
        stationInfo.setRealtimePower(toBigDecimal(info, "powerInter"));
        stationInfo.setInstalledCapacity(toBigDecimal(info, "installCapacity"));
        stationInfo.setDailyGeneration(toBigDecimal(info, "dayGeneration"));
        stationInfo.setDeviceCount(info.getInteger("collects"));
        stationInfo.setAddress(info.getString("address"));
        stationInfo.setStationImage(info.getString("stationImage"));
        if (base != null) {
            stationInfo.setStationName(firstNotBlank(base.getString("stationName"), stationInfo.getStationName()));
            stationInfo.setInstalledCapacity(firstNotNull(
                    toBigDecimal(base, "installCapacity"), stationInfo.getInstalledCapacity()));
            stationInfo.setAreaName(base.getString("areaName"));
            stationInfo.setCityName(base.getString("cityName"));
            stationInfo.setElecPrice(base.getString("elecPrice"));
            stationInfo.setLatitude(base.getString("latitude"));
            stationInfo.setLongitude(base.getString("longitude"));
            stationInfo.setPhone(firstNotBlank(base.getString("Phone"), base.getString("phone")));
            stationInfo.setProvinceName(base.getString("provinceName"));
            stationInfo.setAddress(firstNotBlank(
                    base.getString("stationAddressDetail"), stationInfo.getAddress()));
            stationInfo.setLoanStatus(base.getInteger("loanStatus"));
            stationInfo.setGridStatus(Objects.equals(stationInfo.getLoanStatus(), 3) ? "1" : "0");
            stationInfo.setDataTime(parseDataTime(base.getString("saveTime")));
        }
        stationInfo.setDataSource(2);
        stationInfo.setSyncTime(LocalDateTime.now());
        fillDefaults(stationInfo);
        return stationInfo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addStationInfo(TStationInfo stationInfo) {
        validate(stationInfo);
        stationInfo.setStationId("1"+System.currentTimeMillis());
        stationInfo.setDataSource(1);
        if (lambdaQuery().eq(TStationInfo::getPlatformId,
                stationInfo.getPlatformId() == null ? 0L : stationInfo.getPlatformId())
                .eq(TStationInfo::getStationId, stationInfo.getStationId()).count() > 0) {
            throw new ServiceException("该平台下的电站信息已存在");
        }
        fillDefaults(stationInfo);
        stationInfo.setId(null);
        stationInfo.setCreateTime(LocalDateTime.now());
        stationInfo.setUpdateTime(LocalDateTime.now());
        return save(stationInfo);
    }
    @Override
    public PageInfo<TStationInfo> pageList(StationInfoQuery query) {
        if (query == null) query = new StationInfoQuery();
        PageInfo<TStationInfo> page = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        lambdaQuery()
                .eq(query.getPlatformId() != null, TStationInfo::getPlatformId, query.getPlatformId())
                .like(StringUtils.isNotBlank(query.getStationName()), TStationInfo::getStationName, query.getStationName())
                .eq(StringUtils.isNotBlank(query.getStationType()), TStationInfo::getStationType, query.getStationType())
                .eq(StringUtils.isNotBlank(query.getRunStatus()), TStationInfo::getRunStatus, query.getRunStatus())
                .eq(StringUtils.isNotBlank(query.getGridStatus()), TStationInfo::getGridStatus, query.getGridStatus())
                .orderByAsc(TStationInfo::getSortOrder).orderByDesc(TStationInfo::getCreateTime).page(page);
        return page;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateStationInfo(TStationInfo stationInfo) {
        if (stationInfo == null || stationInfo.getId() == null) throw new ServiceException("电站信息ID不能为空");
        TStationInfo old = getById(stationInfo.getId());
        if (old == null) throw new ServiceException("电站信息不存在");
        if (stationInfo.getStationId() != null && lambdaQuery()
                .eq(TStationInfo::getPlatformId, stationInfo.getPlatformId() == null ? old.getPlatformId() : stationInfo.getPlatformId())
                .eq(TStationInfo::getStationId, stationInfo.getStationId()).ne(TStationInfo::getId, stationInfo.getId()).count() > 0) {
            throw new ServiceException("该平台下的电站信息已存在");
        }
        stationInfo.setCreateBy(null); stationInfo.setCreateTime(null); stationInfo.setDelFlag(null);
        stationInfo.setUpdateTime(LocalDateTime.now());
        return updateById(stationInfo);
    }
    private void validate(TStationInfo stationInfo) {
        if (stationInfo == null) {
            throw new ServiceException("电站信息参数不能为空");
        }
        if (StringUtils.isBlank(stationInfo.getStationName())) {
            throw new ServiceException("电站名称不能为空");
        }
    }
    private void fillDefaults(TStationInfo stationInfo) {
        stationInfo.setPlatformId(stationInfo.getPlatformId() == null ? 0L : stationInfo.getPlatformId());
        stationInfo.setStationType(StringUtils.isBlank(stationInfo.getStationType()) ? "1" : stationInfo.getStationType());
        stationInfo.setRunStatus(StringUtils.isBlank(stationInfo.getRunStatus()) ? "0" : stationInfo.getRunStatus());
        stationInfo.setInstalledCapacity(stationInfo.getInstalledCapacity() == null
                ? BigDecimal.ZERO : stationInfo.getInstalledCapacity());
        stationInfo.setRealtimePower(stationInfo.getRealtimePower() == null
                ? BigDecimal.ZERO : stationInfo.getRealtimePower());
        stationInfo.setDeviceCount(stationInfo.getDeviceCount() == null ? 0 : stationInfo.getDeviceCount());
        stationInfo.setDailyGeneration(stationInfo.getDailyGeneration() == null
                ? BigDecimal.ZERO : stationInfo.getDailyGeneration());
        stationInfo.setGridStatus(StringUtils.isBlank(stationInfo.getGridStatus()) ? "0" : stationInfo.getGridStatus());
        stationInfo.setSortOrder(stationInfo.getSortOrder() == null ? 0 : stationInfo.getSortOrder());
        stationInfo.setCreateBy(StringUtils.defaultString(stationInfo.getCreateBy()));
        stationInfo.setUpdateBy(StringUtils.defaultString(stationInfo.getUpdateBy()));
        stationInfo.setDelFlag("0");
    }
    private Map<String, JSONObject> buildBaseMap(JSONArray jsonArray) {
        Map<String, JSONObject> baseMap = new HashMap<>();
        if (jsonArray != null) {
            for (Object item : jsonArray) {
                JSONObject object = JSONObject.parseObject(item.toString());
                String stationId = object.getString("stationId");
                if (StringUtils.isNotBlank(stationId)) {
                    baseMap.put(stationId, object);
                }
            }
        }
        return baseMap;
    }
    private Long parseStationId(String stationId) {
        try {
            return Long.valueOf(stationId);
        } catch (NumberFormatException exception) {
            log.error("stationId格式不正确:" + stationId);
            return null;
        }
    }
    private BigDecimal toBigDecimal(JSONObject object, String key) {
        if (object == null || object.get(key) == null || StringUtils.isBlank(object.getString(key))) {
            return null;
        }
        try {
            return new BigDecimal(object.getString(key));
        } catch (NumberFormatException exception) {
            log.error(key + "格式不正确:" + object.getString(key));
            return null;
        }
    }
    private LocalDateTime parseDataTime(String value) {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        DateTimeFormatter[] dateTimeFormatters = {
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
        };
        for (DateTimeFormatter formatter : dateTimeFormatters) {
            try {
                return LocalDateTime.parse(value, formatter);
            } catch (DateTimeParseException ignored) {
                // 尝试下一个时间格式
            }
        }
        try {
            return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay();
        } catch (DateTimeParseException exception) {
           log.error("saveTime格式不正确:" + value);
           return null;
        }
    }
    private String firstNotBlank(String preferred, String fallback) {
        return StringUtils.isNotBlank(preferred) ? preferred : fallback;
    }
    private <T> T firstNotNull(T preferred, T fallback) {
        return preferred != null ? preferred : fallback;
    }
    /**
     * 实时获取电站状态
     */
    public StationInfoStatusVO getStationStatus() {
        StationInfoStatusVO statusVO = new StationInfoStatusVO();
        String token = KsolarUtils.token();
        JSONObject statusJsonObj = KsolarUtils.getListStatus(token);
        JSONObject earnJsonObj = KsolarUtils.getListEarn("", token);
        if(Objects.nonNull(statusJsonObj)) {
            statusVO.setAlarm(statusJsonObj.getInteger("alarm"));
            statusVO.setOffline(statusJsonObj.getInteger("offline"));
            statusVO.setOnline(statusJsonObj.getInteger("online"));
            statusVO.setTotal(statusJsonObj.getInteger("total"));
        }
        if(Objects.nonNull(earnJsonObj)) {
            statusVO.setRealPower(earnJsonObj.getDouble("realPower"));
            statusVO.setDayGeneration(earnJsonObj.getDouble("dayGeneration"));
        }
        // 统计电站数量
        List<TStationInfo> stationInfoList =  lambdaQuery().eq(TStationInfo::getDelFlag, "0").list();
        if(CollectionUtils.isNotEmpty(stationInfoList)) {
            statusVO.setStationCount(stationInfoList.size());
            List<TStationInfo>  onStatusList = stationInfoList.stream().filter(s -> Objects.nonNull(s.getLoanStatus()) && s.getLoanStatus() == 3).collect(Collectors.toList());
            statusVO.setOnStatusCount(onStatusList.size());
            statusVO.setOffStatusCount(stationInfoList.size() - onStatusList.size());
        }
        return statusVO;
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java
@@ -1,6 +1,7 @@
package com.ruoyi.other.task;
import com.ruoyi.other.service.TExchangeCodePackageService;
import com.ruoyi.other.service.TStationInfoService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -12,8 +13,19 @@
    @Resource
    private TExchangeCodePackageService exchangeCodePackageService;
    @Resource
    private TStationInfoService tStationInfoService;
    @Scheduled(cron = "0 */5 * * * ?")
    public void refreshExpiredPackageCodes() {
        exchangeCodePackageService.refreshExpiredPackageCodes();
    }
    /**
     * 每天11点执行科士达的数据同步
     */
    @Scheduled(cron = "0 0 23 * * ?")
    public void syncKsolarDataTask() {
        tStationInfoService.syncKsolarDataTask();
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/KsolarUtils.java
@@ -4,27 +4,21 @@
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.other.api.domain.TSystemConfiguration;
import com.ruoyi.other.mapper.TSystemConfigurationMapper;
import com.ruoyi.other.vo.ScreenStorageConfigVO;
import org.springframework.scheduling.annotation.Scheduled;
import com.ruoyi.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
/**
 * 定时任务:储能放电情况 光伏发电情况
 */
@Slf4j
@Service
public class KsolarUtils {
@@ -36,6 +30,7 @@
    private static List<String> stationIds= Arrays.asList("2025060710462417","2025060711331323","2025060715425437","2025060716010425");
    private static final String KSOLAR_URL = "http://111.230.136.62:8092";
    /**
     * 获取今日总发电量
@@ -175,6 +170,7 @@
        post.basicAuth("kstar","kstarSecret");
        post.form("username", "环岛光伏");
        post.form("password", "cR8GGwIXMI462A8Dfl9eXg==");
        System.out.println(post);
        HttpResponse execute = post.execute();
        String body = execute.body();
        JSONObject jsonObject = JSONObject.parseObject(body);
@@ -234,11 +230,155 @@
        }
        return valueTotal+"_"+value;
    }
    public static void main(String[] args) throws Exception {
        String electricity = getAccessToken();
        System.out.println(electricity);
    /**
     * 获取用户的发电量和收益统计
     */
    public static JSONObject getListEarn(String time, String token) {
        try {
            HttpRequest get = HttpUtil.createGet(KSOLAR_URL.concat("/station/list/earn"));
            get.header("Authorization", "Bearer "+token);
            if(StringUtils.isNotEmpty(time)) {
                get.form("startDate", time);
            }
            HttpResponse execute = get.execute();
            String body = execute.body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            String code = jsonObject.get("code").toString();
            if("200".equals(code)){
                Object data  = jsonObject.get("data");
                return JSONObject.parseObject(data.toString());
            }
            return null;
        } catch (Exception e) {
            log.error("获取用户的发电量和收益统计-调用第三方接口报错:", e);
            return null;
        }
    }
    /**
     * 获取用户的电站状态统计
     */
    public static JSONObject getListStatus(String token) {
        try {
            HttpRequest get = HttpUtil.createGet(KSOLAR_URL.concat("/station/list/status"));
            get.header("Authorization", "Bearer "+token);
            HttpResponse execute = get.execute();
            String body = execute.body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            String code = jsonObject.get("code").toString();
            if("200".equals(code)){
                Object data  = jsonObject.get("data");
                return JSONObject.parseObject(data.toString());
            }
           return null;
        } catch (Exception e) {
            log.error("获取用户的电站状态统计-调用第三方接口报错:", e);
            return null;
        }
    }
    /**
     * 电站列表信息
     */
    public static JSONArray getStationListInfo(Integer pageNumber, Integer pageSize, String token) {
        try {
            HttpRequest get = HttpUtil.createGet(KSOLAR_URL.concat("/station/list/info"));
            get.header("Authorization", "Bearer "+token);
            get.form("pageNumber", pageNumber);
            get.form("pageSize", pageSize);
            HttpResponse execute = get.execute();
            String body = execute.body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            String code = jsonObject.get("code").toString();
            if("0".equals(code)){
                String rows = jsonObject.get("rows").toString();
                return JSONArray.parseArray(rows);
            }
            return null;
        } catch (Exception e) {
            log.error("电站列表信息-调用第三方接口报错:", e);
            return null;
        }
    }
    /**
     * 电站基本信息列表
     */
    public static JSONArray getStationListBase(Integer pageNumber, Integer pageSize, String token) {
        try {
            HttpRequest post = HttpUtil.createPost(KSOLAR_URL.concat("/station/list/base"));
            post.header("Authorization", "Bearer "+token);
            post.header("Content-Type", "application/json");
            JSONObject param = new JSONObject();
            param.put("pageNumber", pageNumber);
            param.put("pageSize", pageSize);
            HttpResponse execute = post.body(param.toJSONString()).execute();
            String body = execute.body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            String code = jsonObject.get("code").toString();
            if("0".equals(code)){
                String rows = jsonObject.get("rows").toString();
                return JSONArray.parseArray(rows);
            }
            return null;
        } catch (Exception e) {
            log.error("电站列表信息-调用第三方接口报错:", e);
            return null;
        }
    }
    /**
     * 逆变器列表
     */
    public static JSONArray getCollectBaseList(Integer pageNumber, Integer pageSize, String token) {
        try {
            HttpRequest post = HttpUtil.createPost(KSOLAR_URL.concat("/collect/manager/base/list"));
            post.header("Authorization", "Bearer "+token);
            post.header("Content-Type", "application/json");
            JSONObject param = new JSONObject();
            param.put("pageNumber", pageNumber);
            param.put("pageSize", pageSize);
            HttpResponse execute = post.body(param.toJSONString()).execute();
            String body = execute.body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            String code = jsonObject.get("code").toString();
            if("0".equals(code)){
                String rows = jsonObject.get("rows").toString();
                return JSONArray.parseArray(rows);
            }
            return null;
        } catch (Exception e) {
            log.error("逆变器列表-调用第三方接口报错:", e);
            return null;
        }
    }
    /**
     * 电站发电量和收益统计明细
     */
    public static JSONArray getStationElecList(String startDate, String endDate, String token) {
        try {
            HttpRequest get = HttpUtil.createGet(KSOLAR_URL.concat("/protocol/station/statistic/elec/detail"));
            get.header("Authorization", "Bearer "+token);
            if(StringUtils.isNotEmpty(startDate)) {
                get.form("startDate", startDate);
            }
            if(StringUtils.isNotEmpty(endDate)) {
                get.form("endDate", endDate);
            }
            HttpResponse execute = get.execute();
            String body = execute.body();
            return JSONArray.parseArray(body);
        } catch (Exception e) {
            log.error("电站的电量和收益统计查询-调用第三方接口报错:", e);
            return null;
        }
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnExportVO.java
New file
@@ -0,0 +1,32 @@
package com.ruoyi.other.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Objects;
@Data
public class StationEarnExportVO {
    @Excel(name = "平台", width = 15) private String platformName;
    @Excel(name = "电站ID", width = 24) private String stationId;
    @Excel(name = "电站名称", width = 25) private String stationName;
    @Excel(name = "统计日期", width = 18) private String saveTime;
    @Excel(name = "总发电量(度)", width = 20) private BigDecimal elecTotal;
    @Excel(name = "总收益(元)", width = 20) private BigDecimal earnTotal;
    public void buildPlatformName(Long platformId) {
       // 0科士达,1华为,2阳光,3古瑞瓦特
        if(Objects.nonNull(platformId)) {
            if(platformId == 0) {
                this.setPlatformName("科士达");
            }else if(platformId == 1) {
                this.setPlatformName("华为");
            }else if(platformId == 2) {
                this.setPlatformName("阳光");
            }else if(platformId == 3) {
                this.setPlatformName("古瑞瓦特");
            }
        }
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnOverviewVO.java
New file
@@ -0,0 +1,21 @@
package com.ruoyi.other.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel("电站发电量概览")
public class StationEarnOverviewVO {
    @ApiModelProperty("今日发电量(度)")
    private BigDecimal todayGeneration;
    @ApiModelProperty("本月发电量(度)")
    private BigDecimal monthGeneration;
    @ApiModelProperty("本年发电量(万度)")
    private BigDecimal yearGeneration;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnStatisticsItemVO.java
New file
@@ -0,0 +1,25 @@
package com.ruoyi.other.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel("电站发电量统计图表数据项")
public class StationEarnStatisticsItemVO {
    @ApiModelProperty("横轴标签:日期、月份或年份")
    private String label;
    @ApiModelProperty("发电量(度/kWh)")
    private BigDecimal generation;
    @ApiModelProperty("收益(元)")
    private BigDecimal earn;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationEarnStatisticsVO.java
New file
@@ -0,0 +1,25 @@
package com.ruoyi.other.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@ApiModel("电站发电量统计结果")
public class StationEarnStatisticsVO {
    @ApiModelProperty("统计维度:day、month、year")
    private String dimension;
    @ApiModelProperty("合计发电量(度/kWh)")
    private BigDecimal totalGeneration;
    @ApiModelProperty("合计收益(元)")
    private BigDecimal totalEarn;
    @ApiModelProperty("图表数据")
    private List<StationEarnStatisticsItemVO> items;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/StationInfoStatusVO.java
New file
@@ -0,0 +1,39 @@
package com.ruoyi.other.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel("电站状态")
public class StationInfoStatusVO {
    @ApiModelProperty("告警个数")
    private Integer alarm;
    @ApiModelProperty("离线个数")
    private Integer offline;
    @ApiModelProperty("在线个数")
    private Integer online;
    @ApiModelProperty("电站总数")
    private Integer total;
    @ApiModelProperty("实时功率")
    private Double realPower;
    @ApiModelProperty("日发电量")
    private Double dayGeneration;
    @ApiModelProperty("电站数量")
    private Integer stationCount;
    @ApiModelProperty("已上网数量")
    private Integer onStatusCount;
    @ApiModelProperty("未上网数量")
    private Integer offStatusCount;
}
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TStationCollectMapper.xml
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.other.mapper.TStationCollectMapper">
</mapper>
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TStationEarnMapper.xml
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.other.mapper.TStationEarnMapper">
</mapper>
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TStationInfoMapper.xml
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.other.mapper.TStationInfoMapper">
</mapper>
ruoyi-service/ruoyi-other/src/main/resources/sql/t_station_collect.sql
New file
@@ -0,0 +1,62 @@
-- ----------------------------
-- 电站逆变器表
-- ----------------------------
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `t_station_collect` (
       `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
       `platform_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属平台ID(0科士达,1华为,2阳光,3古瑞瓦特)',
       `station_id` VARCHAR(100) DEFAULT NULL COMMENT '电站ID',
       `station_name` VARCHAR(255) DEFAULT NULL COMMENT '电站名称',
       `collect_id` VARCHAR(100) NOT NULL COMMENT '逆变器ID',
       `collect_name` VARCHAR(255) NOT NULL COMMENT '逆变器名称',
       `device_model` VARCHAR(200) DEFAULT NULL COMMENT '机型型号',
       `inverter_id` VARCHAR(200) DEFAULT NULL COMMENT '逆变器SN',
       `installed_capacity` VARCHAR(100) DEFAULT NULL COMMENT '装机容量(kW)',
       `power_num` INT DEFAULT 0 COMMENT '装机容量',
       `province_name` VARCHAR(200) DEFAULT NULL COMMENT '省',
       `city_name` VARCHAR(200) DEFAULT NULL COMMENT '市',
       `area_name` VARCHAR(200) DEFAULT NULL COMMENT '区/县',
       `address` VARCHAR(1000) DEFAULT NULL COMMENT '详细地址',
       `latitude` VARCHAR(100) DEFAULT NULL COMMENT '纬度',
       `longitude` VARCHAR(100) DEFAULT NULL COMMENT '经度',
       `status` CHAR(1) DEFAULT '0' COMMENT '状态(1在线 2离线 3故障)',
       `data_source` INT DEFAULT NULL COMMENT '数据来源(1系统创建,2第三方接口)',
       `sort_order` INT DEFAULT 0 COMMENT '显示顺序',
       `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
       `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
       `sync_time` DATETIME DEFAULT NULL COMMENT '同步时间',
       `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
       `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
       `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
       `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
       PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='电站逆变器表';
ruoyi-service/ruoyi-other/src/main/resources/sql/t_station_earn.sql
New file
@@ -0,0 +1,24 @@
-- ----------------------------
-- 发电量表
-- 对应发电量分析中的发电量、收益和碳减排统计数据
-- ----------------------------
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `t_station_earn` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `platform_id` bigint NOT NULL DEFAULT 0 COMMENT '平台ID,0表示科士达',
  `station_id` varchar(100) NULL COMMENT '电站ID',
  `station_name` varchar(100) NULL COMMENT '电站名称',
  `save_time` date NOT NULL COMMENT '时间',
  `elec_total` decimal(18,4) Null DEFAULT 0.0000 COMMENT '总发电量',
  `earn_total` decimal(18,4) Null DEFAULT 0.0000 COMMENT '总收益',
  `create_by` varchar(64) NULL DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
    data_source        int  default NULL   null comment '数据来源(1系统创建,2第三方接口)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='发电量统计表';
ruoyi-service/ruoyi-other/src/main/resources/sql/t_station_info.sql
New file
@@ -0,0 +1,44 @@
-- ----------------------------
-- 电站明细表
-- 对应电站概览卡片中的基础信息、运行状态及发电指标
-- ----------------------------
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `charging_pile_other`.`t_station_info` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `platform_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属平台ID(0科十达,1华为,2阳光,3古瑞瓦特)',
    `station_id` VARCHAR(100) NOT NULL COMMENT '电站ID',
    `station_name` VARCHAR(255) NOT NULL COMMENT '电站名称',
    `station_image` VARCHAR(500) DEFAULT NULL COMMENT '电站图片地址',
    `station_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '电站类型(1光伏、2储能)',
    `run_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '运行状态(0正常/在线 1离线 2告警)',
    `installed_capacity` DECIMAL(18,4) NOT NULL DEFAULT 0.0000 COMMENT '装机容量(kW)',
    `realtime_power` DECIMAL(18,4) NOT NULL DEFAULT 0.0000 COMMENT '实时功率(W)',
    `device_count` INT NOT NULL DEFAULT 0 COMMENT '设备总数',
    `daily_generation` DECIMAL(18,4) NOT NULL DEFAULT 0.0000 COMMENT '日发电量(度/kWh)',
    `grid_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '上网状态(0未上网 1已上网)',
    `data_time` DATETIME DEFAULT NULL COMMENT '指标数据时间',
    `elec_price` VARCHAR(100) DEFAULT NULL COMMENT '电价',
    `contact_name` VARCHAR(255) DEFAULT NULL COMMENT '联系人',
    `phone` VARCHAR(50) DEFAULT NULL COMMENT '电话',
    `province_name` VARCHAR(200) DEFAULT NULL COMMENT '省',
    `city_name` VARCHAR(200) DEFAULT NULL COMMENT '市',
    `area_name` VARCHAR(200) DEFAULT NULL COMMENT '区/县',
    `address` VARCHAR(1000) DEFAULT NULL COMMENT '详细地址',
    `latitude` VARCHAR(100) DEFAULT NULL COMMENT '纬度',
    `longitude` VARCHAR(100) DEFAULT NULL COMMENT '经度',
    `loan_status` INT DEFAULT NULL COMMENT '并网状态(1未并网,2并网中,3已并网)',
    `data_source` INT DEFAULT NULL COMMENT '数据来源(1系统创建,2第三方接口)',
    `sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示顺序',
    `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
    `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
    `sync_time` DATETIME DEFAULT NULL COMMENT '同步时间',
    `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
    `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
    `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='电站信息表';