1.调试ai
This commit is contained in:
19
sql/mysql/ydoyun_ai_assistant_report_detail.sql
Normal file
19
sql/mysql/ydoyun_ai_assistant_report_detail.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 每日汇报详表:与 ydoyun_ai_assistant_report 一对多
|
||||
CREATE TABLE IF NOT EXISTS `ydoyun_ai_assistant_report_detail` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`report_id` bigint NOT NULL COMMENT '每日汇报主表ID(ydoyun_ai_assistant_report.id)',
|
||||
`diagnosis_item` varchar(256) NOT NULL DEFAULT '' COMMENT '诊断项目',
|
||||
`value1` varchar(512) DEFAULT '' COMMENT '指标异常',
|
||||
`value2` varchar(512) DEFAULT '' COMMENT '归因分析',
|
||||
`value3` varchar(512) DEFAULT '' COMMENT '改善对策',
|
||||
`sort_order` int NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`creator` varchar(64) DEFAULT '' NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
|
||||
`updater` varchar(64) DEFAULT '' NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` bit(1) DEFAULT b'0' NOT NULL COMMENT '是否删除',
|
||||
`tenant_id` bigint DEFAULT 0 NOT NULL COMMENT '租户编号',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_report_id` (`report_id`),
|
||||
KEY `idx_tenant_id` (`tenant_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI决策助手每日汇报详表';
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 高德开放平台 Web 服务:地理编码 + 天气查询。
|
||||
* <a href="https://lbs.amap.com/api/webservice/guide/api/weatherinfo">天气查询</a>
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "ydoyun.amap.weather")
|
||||
public class AmapWeatherProperties {
|
||||
|
||||
/**
|
||||
* 高德「Web 服务」类型 Key(勿与 JS API Key 混用)
|
||||
*/
|
||||
private String key = "";
|
||||
|
||||
/**
|
||||
* 地理编码接口,默认官方 v3
|
||||
*/
|
||||
private String geocodeUrl = "https://restapi.amap.com/v3/geocode/geo";
|
||||
|
||||
/**
|
||||
* 天气查询接口,默认官方 v3
|
||||
*/
|
||||
private String weatherUrl = "https://restapi.amap.com/v3/weather/weatherInfo";
|
||||
|
||||
/**
|
||||
* 行政区划查询(城市名→adcode,地理编码失败时的回退)
|
||||
*/
|
||||
private String districtUrl = "https://restapi.amap.com/v3/config/district";
|
||||
|
||||
/**
|
||||
* 天气图标 URL 模板,必须包含占位符 {@code {code}}(OpenWeatherMap 图标名,如 01d、10d)。
|
||||
* 默认使用 OpenWeatherMap 官方 CDN;若环境无法访问外网,可改为相对路径,例如:
|
||||
* {@code /weather-icons/{code}.png},并将图标放入前端 {@code public/weather-icons/}(文件名 01d.png、10d.png 等)。
|
||||
* 设为空字符串则不返回图标。
|
||||
*/
|
||||
private String weatherIconUrlPattern = "/weather-icons/{code}.png";
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "每日汇报详表批量保存(全量替换)")
|
||||
@Data
|
||||
public class AiAssistantReportDetailBatchSaveReqVO {
|
||||
|
||||
@Schema(description = "每日汇报主表 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "报告主键不能为空")
|
||||
private Long reportId;
|
||||
|
||||
@Schema(description = "详表行列表(空行会在服务端忽略;先删后插)")
|
||||
@Valid
|
||||
private List<AiAssistantReportDetailSaveItemVO> details;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - AI 决策助手每日汇报详表")
|
||||
@Data
|
||||
public class AiAssistantReportDetailRespVO {
|
||||
|
||||
@Schema(description = "主键")
|
||||
private Long id;
|
||||
@Schema(description = "每日汇报主表 ID")
|
||||
private Long reportId;
|
||||
@Schema(description = "诊断项目")
|
||||
private String diagnosisItem;
|
||||
@Schema(description = "指标异常")
|
||||
private String value1;
|
||||
@Schema(description = "归因分析")
|
||||
private String value2;
|
||||
@Schema(description = "改善对策")
|
||||
private String value3;
|
||||
@Schema(description = "排序")
|
||||
private Integer sortOrder;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "每日汇报详表单行保存项")
|
||||
@Data
|
||||
public class AiAssistantReportDetailSaveItemVO {
|
||||
|
||||
@Schema(description = "诊断项目")
|
||||
private String diagnosisItem;
|
||||
@Schema(description = "指标异常")
|
||||
private String value1;
|
||||
@Schema(description = "归因分析")
|
||||
private String value2;
|
||||
@Schema(description = "改善对策")
|
||||
private String value3;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "每日汇报 - Dify 智能解析报告正文")
|
||||
@Data
|
||||
public class AiAssistantReportDifyParseReqVO {
|
||||
|
||||
@Schema(description = "报告内容全文", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "报告内容不能为空")
|
||||
private String text;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.weather;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.weather.vo.WeatherAggregateRespVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.service.weather.AmapWeatherService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 高德天气:服务端代理地理编码 + 实况 + 预报,避免浏览器直连 Key。
|
||||
* 路径沿用 /wttr,返回结构为 {@link WeatherAggregateRespVO}。
|
||||
*/
|
||||
@Tag(name = "管理后台 - 天气代理")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ydoyun/weather")
|
||||
public class WeatherWttrController {
|
||||
|
||||
@Resource
|
||||
private AmapWeatherService amapWeatherService;
|
||||
|
||||
@GetMapping("/wttr")
|
||||
@Operation(summary = "高德天气:按城市名或 adcode 聚合实况 + 近 3 日预报")
|
||||
public CommonResult<WeatherAggregateRespVO> getWeather(@RequestParam("location") String location) {
|
||||
if (location == null || location.trim().isEmpty()) {
|
||||
return error(400, "location 不能为空");
|
||||
}
|
||||
try {
|
||||
return success(amapWeatherService.getAggregate(location.trim()));
|
||||
} catch (IllegalStateException e) {
|
||||
log.warn("[weather] 高德天气业务失败 location={} msg={}", location, e.getMessage());
|
||||
return error(502, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[weather] 高德天气请求失败 location={}", location, e);
|
||||
return error(502, "获取天气失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.weather.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "销售日报等页面使用的天气聚合数据(高德实况 + 预报)")
|
||||
@Data
|
||||
public class WeatherAggregateRespVO {
|
||||
|
||||
@Schema(description = "展示用城市名")
|
||||
private String cityLabel;
|
||||
|
||||
@Schema(description = "实况")
|
||||
private Current current;
|
||||
|
||||
@Schema(description = "未来 3 天预报")
|
||||
private List<Day> days;
|
||||
|
||||
@Data
|
||||
public static class Current {
|
||||
@Schema(description = "温度(字符串)")
|
||||
private String temp;
|
||||
@Schema(description = "天气文字")
|
||||
private String desc;
|
||||
@Schema(description = "图标完整 URL(CDN)")
|
||||
private String icon;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Day {
|
||||
@Schema(description = "预报日期 yyyy-MM-dd")
|
||||
private String fxDate;
|
||||
@Schema(description = "周几,如 周二")
|
||||
private String weekLabel;
|
||||
@Schema(description = "月/日,如 3/25")
|
||||
private String dateLabel;
|
||||
@Schema(description = "最低~最高,如 12~22°C")
|
||||
private String range;
|
||||
@Schema(description = "白天天气简述")
|
||||
private String desc;
|
||||
@Schema(description = "白天图标 URL")
|
||||
private String icon;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.dal.dataobject.aiassistantreport;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* AI 决策助手每日汇报详表 DO(与主表一对多)
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@TableName("ydoyun_ai_assistant_report_detail")
|
||||
@KeySequence("ydoyun_ai_assistant_report_detail_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiAssistantReportDetailDO extends TenantBaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
/** 主表 ID */
|
||||
private Long reportId;
|
||||
/** 诊断项目 */
|
||||
private String diagnosisItem;
|
||||
/** 指标异常 */
|
||||
private String value1;
|
||||
/** 归因分析 */
|
||||
private String value2;
|
||||
/** 改善对策 */
|
||||
private String value3;
|
||||
/** 排序 */
|
||||
private Integer sortOrder;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.dal.mysql.aiassistantreport;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aiassistantreport.AiAssistantReportDetailDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 决策助手每日汇报详表 Mapper
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAssistantReportDetailMapper extends BaseMapperX<AiAssistantReportDetailDO> {
|
||||
|
||||
default List<AiAssistantReportDetailDO> selectListByReportId(Long reportId) {
|
||||
return selectList(new LambdaQueryWrapperX<AiAssistantReportDetailDO>()
|
||||
.eq(AiAssistantReportDetailDO::getReportId, reportId)
|
||||
.orderByAsc(AiAssistantReportDetailDO::getSortOrder)
|
||||
.orderByAsc(AiAssistantReportDetailDO::getId));
|
||||
}
|
||||
|
||||
default int deleteByReportId(Long reportId) {
|
||||
return delete(new LambdaQueryWrapperX<AiAssistantReportDetailDO>()
|
||||
.eq(AiAssistantReportDetailDO::getReportId, reportId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aiassistantreport;
|
||||
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailBatchSaveReqVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailRespVO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 每日汇报详表 Service
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
public interface AiAssistantReportDetailService {
|
||||
|
||||
/**
|
||||
* 按主表 ID 查询详表(校验报告归属当前登录人)
|
||||
*/
|
||||
List<AiAssistantReportDetailRespVO> listByReportId(@NotNull Long reportId, @NotNull Long loginUserId);
|
||||
|
||||
/**
|
||||
* 批量保存(删除该主表下旧明细后插入新行)
|
||||
*/
|
||||
void saveBatch(@Valid AiAssistantReportDetailBatchSaveReqVO reqVO, @NotNull Long loginUserId);
|
||||
|
||||
/**
|
||||
* 按主表 ID 删除所有明细(逻辑删除)
|
||||
*/
|
||||
void deleteByReportId(@NotNull Long reportId);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aiassistantreport;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailBatchSaveReqVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailRespVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailSaveItemVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aiassistantreport.AiAssistantReportDO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aiassistantreport.AiAssistantReportDetailDO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.mysql.aiassistantreport.AiAssistantReportDetailMapper;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.mysql.aiassistantreport.AiAssistantReportMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* 每日汇报详表 Service 实现
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class AiAssistantReportDetailServiceImpl implements AiAssistantReportDetailService {
|
||||
|
||||
@Resource
|
||||
private AiAssistantReportMapper aiAssistantReportMapper;
|
||||
@Resource
|
||||
private AiAssistantReportDetailMapper aiAssistantReportDetailMapper;
|
||||
|
||||
@Override
|
||||
public List<AiAssistantReportDetailRespVO> listByReportId(Long reportId, Long loginUserId) {
|
||||
AiAssistantReportDO report = aiAssistantReportMapper.selectById(reportId);
|
||||
if (report == null) {
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
if (!loginUserId.equals(report.getReporterId())) {
|
||||
throw exception(FORBIDDEN);
|
||||
}
|
||||
List<AiAssistantReportDetailDO> list = aiAssistantReportDetailMapper.selectListByReportId(reportId);
|
||||
return BeanUtils.toBean(list, AiAssistantReportDetailRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveBatch(AiAssistantReportDetailBatchSaveReqVO reqVO, Long loginUserId) {
|
||||
Long reportId = reqVO.getReportId();
|
||||
AiAssistantReportDO report = aiAssistantReportMapper.selectById(reportId);
|
||||
if (report == null) {
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
if (!loginUserId.equals(report.getReporterId())) {
|
||||
throw exception(FORBIDDEN);
|
||||
}
|
||||
aiAssistantReportDetailMapper.deleteByReportId(reportId);
|
||||
List<AiAssistantReportDetailSaveItemVO> details = reqVO.getDetails();
|
||||
if (details == null) {
|
||||
details = Collections.emptyList();
|
||||
}
|
||||
int sort = 0;
|
||||
for (AiAssistantReportDetailSaveItemVO item : details) {
|
||||
if (isAllBlank(item)) {
|
||||
continue;
|
||||
}
|
||||
AiAssistantReportDetailDO row = new AiAssistantReportDetailDO();
|
||||
row.setReportId(reportId);
|
||||
row.setDiagnosisItem(nullToEmpty(item.getDiagnosisItem()));
|
||||
row.setValue1(nullToEmpty(item.getValue1()));
|
||||
row.setValue2(nullToEmpty(item.getValue2()));
|
||||
row.setValue3(nullToEmpty(item.getValue3()));
|
||||
row.setSortOrder(sort++);
|
||||
aiAssistantReportDetailMapper.insert(row);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByReportId(Long reportId) {
|
||||
aiAssistantReportDetailMapper.deleteByReportId(reportId);
|
||||
}
|
||||
|
||||
private static boolean isAllBlank(AiAssistantReportDetailSaveItemVO item) {
|
||||
return isBlank(item.getDiagnosisItem()) && isBlank(item.getValue1())
|
||||
&& isBlank(item.getValue2()) && isBlank(item.getValue3());
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String s) {
|
||||
return s == null ? "" : s.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aiassistantreport;
|
||||
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailSaveItemVO;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 每日汇报正文 → Dify 应用解析 → 经营诊断明细行
|
||||
*/
|
||||
public interface DailyReportDifyParseService {
|
||||
|
||||
/**
|
||||
* 调用 Dify blocking 接口,将返回的 JSON 解析为详表行
|
||||
*/
|
||||
List<AiAssistantReportDetailSaveItemVO> parseReportToDiagnosisRows(@NotBlank String text);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aiassistantreport;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aiassistantreport.vo.AiAssistantReportDetailSaveItemVO;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* 调用 Dify Chat 应用(blocking),解析 answer 为经营诊断 JSON 数组
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class DailyReportDifyParseServiceImpl implements DailyReportDifyParseService {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Value("${ydoyun.dify.daily-report-parse.base-url:http://118.253.178.8:5001/v1}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${ydoyun.dify.daily-report-parse.api-key:app-aaqeCTv9ywLOIl4WvhD3P9xS}")
|
||||
private String apiKey;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Override
|
||||
public List<AiAssistantReportDetailSaveItemVO> parseReportToDiagnosisRows(String text) {
|
||||
String answer = callDifyBlocking(text);
|
||||
return parseAnswerToRows(answer);
|
||||
}
|
||||
|
||||
private String callDifyBlocking(String text) {
|
||||
String base = baseUrl.replaceAll("/$", "");
|
||||
String chatUrl = base + "/chat-messages";
|
||||
Long uid = SecurityFrameworkUtils.getLoginUserId();
|
||||
String user = uid != null ? "daily-report-" + uid : "daily-report-anon";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("inputs", new HashMap<>());
|
||||
requestBody.put("query", text);
|
||||
requestBody.put("response_mode", "blocking");
|
||||
requestBody.put("user", user);
|
||||
|
||||
try {
|
||||
String jsonBody = OBJECT_MAPPER.writeValueAsString(requestBody);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(apiKey);
|
||||
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||
|
||||
ResponseEntity<String> resp = restTemplate.exchange(
|
||||
chatUrl,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) {
|
||||
log.error("[DailyReportDify] HTTP 非成功: status={}, body={}", resp.getStatusCode(), resp.getBody());
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
JsonNode root = OBJECT_MAPPER.readTree(resp.getBody().getBytes(StandardCharsets.UTF_8));
|
||||
if (root.has("code") && "400".equals(root.path("code").asText())) {
|
||||
log.warn("[DailyReportDify] 业务错误: {}", resp.getBody());
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
if (root.has("answer")) {
|
||||
return root.get("answer").asText("");
|
||||
}
|
||||
log.warn("[DailyReportDify] 响应无 answer 字段: {}", resp.getBody());
|
||||
throw exception(BAD_REQUEST);
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("[DailyReportDify] 调用失败", e);
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AiAssistantReportDetailSaveItemVO> parseAnswerToRows(String answer) {
|
||||
if (isBlank(answer)) {
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
String s = answer.trim();
|
||||
s = stripMarkdownFence(s);
|
||||
s = extractJsonArray(s);
|
||||
|
||||
try {
|
||||
JsonNode root = OBJECT_MAPPER.readTree(s);
|
||||
JsonNode arr = root;
|
||||
if (root.isObject()) {
|
||||
if (root.has("items") && root.get("items").isArray()) {
|
||||
arr = root.get("items");
|
||||
} else if (root.has("list") && root.get("list").isArray()) {
|
||||
arr = root.get("list");
|
||||
} else if (root.has("data") && root.get("data").isArray()) {
|
||||
arr = root.get("data");
|
||||
} else if (root.has("rows") && root.get("rows").isArray()) {
|
||||
arr = root.get("rows");
|
||||
}
|
||||
}
|
||||
if (!arr.isArray()) {
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
List<AiAssistantReportDetailSaveItemVO> out = new ArrayList<>();
|
||||
for (JsonNode item : arr) {
|
||||
if (!item.isObject()) {
|
||||
continue;
|
||||
}
|
||||
AiAssistantReportDetailSaveItemVO row = new AiAssistantReportDetailSaveItemVO();
|
||||
// AI 标准 JSON:诊断项 / 指标异常 / 归因分析 / 改善对策(与英文字段名兼容)
|
||||
row.setDiagnosisItem(
|
||||
firstText(item, "诊断项", "诊断项目", "diagnosisItem", "diagnosis_item", "title", "name"));
|
||||
row.setValue1(firstText(item, "指标异常", "value1", "indicator", "abnormal"));
|
||||
row.setValue2(firstText(item, "归因分析", "value2", "attribution"));
|
||||
row.setValue3(firstText(item, "改善对策", "value3", "improvement"));
|
||||
if (isAllBlank(row)) {
|
||||
continue;
|
||||
}
|
||||
out.add(row);
|
||||
}
|
||||
// AI 可能直接返回 [] 或有效 JSON 但无有效行:返回空列表,由前端提示「没有异常」
|
||||
return out;
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.warn("[DailyReportDify] JSON 解析失败, raw={}", s, e);
|
||||
throw exception(BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAllBlank(AiAssistantReportDetailSaveItemVO row) {
|
||||
return isBlank(row.getDiagnosisItem()) && isBlank(row.getValue1())
|
||||
&& isBlank(row.getValue2()) && isBlank(row.getValue3());
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String firstText(JsonNode node, String... keys) {
|
||||
for (String k : keys) {
|
||||
if (node.has(k) && !node.get(k).isNull()) {
|
||||
return node.get(k).asText("").trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String stripMarkdownFence(String s) {
|
||||
if (!s.startsWith("```")) {
|
||||
return s;
|
||||
}
|
||||
int nl = s.indexOf('\n');
|
||||
if (nl < 0) {
|
||||
return s;
|
||||
}
|
||||
int end = s.lastIndexOf("```");
|
||||
if (end <= nl) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(nl + 1, end).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从可能含前后说明文字的字符串中截取 JSON 数组 [...]
|
||||
*/
|
||||
private static String extractJsonArray(String s) {
|
||||
int a = s.indexOf('[');
|
||||
int b = s.lastIndexOf(']');
|
||||
if (a >= 0 && b > a) {
|
||||
return s.substring(a, b + 1);
|
||||
}
|
||||
int o1 = s.indexOf('{');
|
||||
int o2 = s.lastIndexOf('}');
|
||||
if (o1 >= 0 && o2 > o1) {
|
||||
return s.substring(o1, o2 + 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.weather;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 高德仅返回中文现象,图标需自行映射。默认使用 OpenWeatherMap 公开图标名(01d、10d 等),由 {@code urlPattern} 拼出最终地址。
|
||||
* <a href="https://openweathermap.org/weather-conditions">图标与现象对照</a>
|
||||
*/
|
||||
final class AmapWeatherIconMapper {
|
||||
|
||||
private AmapWeatherIconMapper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param chineseWeather 如「阴」「阵雨」
|
||||
* @param urlPattern 须包含占位符 {@code {code}},如 {@code https://openweathermap.org/img/wn/{code}@2x.png};
|
||||
* 也可用相对路径 {@code /weather-icons/{code}.png}(图标文件随前端 public 部署)
|
||||
* @return 完整 URL;pattern 为空则返回空串(不展示图标)
|
||||
*/
|
||||
static String iconUrlFromDesc(String chineseWeather, String urlPattern) {
|
||||
if (!StringUtils.hasText(urlPattern) || !StringUtils.hasText(urlPattern.trim())) {
|
||||
return "";
|
||||
}
|
||||
if (!StringUtils.hasText(chineseWeather)) {
|
||||
return "";
|
||||
}
|
||||
String d = chineseWeather.trim();
|
||||
String code = matchOpenWeatherMapIconCode(d);
|
||||
return urlPattern.trim().replace("{code}", code);
|
||||
}
|
||||
|
||||
/** OpenWeatherMap 白天图标代码(统一用 d,避免昼夜判断) */
|
||||
private static String matchOpenWeatherMapIconCode(String d) {
|
||||
String[][] rules = {
|
||||
{"雷阵雨", "11d"},
|
||||
{"阵雨", "09d"},
|
||||
{"暴雨", "10d"},
|
||||
{"大雨", "10d"},
|
||||
{"中雨", "10d"},
|
||||
{"小雨", "10d"},
|
||||
{"雨夹雪", "13d"},
|
||||
{"暴雪", "13d"},
|
||||
{"大雪", "13d"},
|
||||
{"中雪", "13d"},
|
||||
{"小雪", "13d"},
|
||||
{"雨雪", "13d"},
|
||||
{"雪", "13d"},
|
||||
{"冰雹", "11d"},
|
||||
{"冻雨", "13d"},
|
||||
{"雾", "50d"},
|
||||
{"霾", "50d"},
|
||||
{"沙尘暴", "50d"},
|
||||
{"扬沙", "50d"},
|
||||
{"浮尘", "50d"},
|
||||
{"阴", "04d"},
|
||||
{"多云", "03d"},
|
||||
{"晴", "01d"},
|
||||
{"雨", "10d"},
|
||||
};
|
||||
for (String[] r : rules) {
|
||||
if (d.contains(r[0])) {
|
||||
return r[1];
|
||||
}
|
||||
}
|
||||
return "02d";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.weather;
|
||||
|
||||
import cn.iocoder.yudao.module.ydoyun.config.AmapWeatherProperties;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.weather.vo.WeatherAggregateRespVO;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 高德天气:6 位 {@code adcode} 或城市名(先地理编码,失败则行政区划查询)→ 实况 + 预报。
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AmapWeatherService {
|
||||
|
||||
@Resource
|
||||
private RestTemplate restTemplate;
|
||||
@Resource
|
||||
private AmapWeatherProperties properties;
|
||||
|
||||
public WeatherAggregateRespVO getAggregate(String locationKeyword) {
|
||||
String trimmed = locationKeyword == null ? "" : locationKeyword.trim();
|
||||
if (!StringUtils.hasText(trimmed)) {
|
||||
throw new IllegalStateException("location 不能为空");
|
||||
}
|
||||
String apiKey = trimKey(properties.getKey());
|
||||
if (!StringUtils.hasText(apiKey)) {
|
||||
throw new IllegalStateException("未配置高德 Web 服务 Key:ydoyun.amap.weather.key");
|
||||
}
|
||||
|
||||
String adcode = resolveAdcode(trimmed, apiKey);
|
||||
if (!StringUtils.hasText(adcode)) {
|
||||
throw new IllegalStateException("无法解析城市「" + trimmed + "」,请核对名称或传入 6 位行政区划编码 adcode");
|
||||
}
|
||||
|
||||
JSONObject baseRoot = requestWeather(adcode, apiKey, "base");
|
||||
JSONObject allRoot = requestWeather(adcode, apiKey, "all");
|
||||
return buildVo(baseRoot, allRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯城市名用「地理编码」易触发 ENGINE_RESPONSE_DATA_ERROR;失败时改用「行政区划查询」解析 adcode。
|
||||
*/
|
||||
private String resolveAdcode(String keyword, String apiKey) {
|
||||
if (keyword.matches("^\\d{6}$")) {
|
||||
return keyword;
|
||||
}
|
||||
String adcode = tryGeocode(keyword, apiKey);
|
||||
if (StringUtils.hasText(adcode)) {
|
||||
return adcode;
|
||||
}
|
||||
adcode = tryDistrict(keyword, apiKey);
|
||||
if (StringUtils.hasText(adcode)) {
|
||||
return adcode;
|
||||
}
|
||||
adcode = tryInputTipsCityAdcode(keyword, apiKey);
|
||||
if (StringUtils.hasText(adcode)) {
|
||||
return adcode;
|
||||
}
|
||||
// 去掉「市」再试(如 郴州市 → 郴州)
|
||||
if (keyword.endsWith("市") && keyword.length() > 1) {
|
||||
String shortName = keyword.substring(0, keyword.length() - 1);
|
||||
adcode = tryGeocode(shortName, apiKey);
|
||||
if (StringUtils.hasText(adcode)) {
|
||||
return adcode;
|
||||
}
|
||||
adcode = tryDistrict(shortName, apiKey);
|
||||
if (StringUtils.hasText(adcode)) {
|
||||
return adcode;
|
||||
}
|
||||
adcode = tryInputTipsCityAdcode(shortName, apiKey);
|
||||
if (StringUtils.hasText(adcode)) {
|
||||
return adcode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 地理编码:成功返回 adcode,失败返回 null(不抛错,便于走 district) */
|
||||
private String tryGeocode(String keyword, String apiKey) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromHttpUrl(properties.getGeocodeUrl())
|
||||
.queryParam("address", keyword)
|
||||
.queryParam("key", apiKey)
|
||||
.build()
|
||||
.toUriString();
|
||||
String body = restTemplate.getForObject(url, String.class);
|
||||
JSONObject root = body == null ? new JSONObject() : JSONObject.parseObject(body);
|
||||
if (!isAmapHttpSuccess(root)) {
|
||||
log.debug("[amap] geocode 未成功 keyword={} info={}", keyword, root.getString("info"));
|
||||
return null;
|
||||
}
|
||||
JSONArray geocodes = root.getJSONArray("geocodes");
|
||||
if (geocodes == null || geocodes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return geocodes.getJSONObject(0).getString("adcode");
|
||||
} catch (Exception e) {
|
||||
log.warn("[amap] geocode 异常 keyword={}", keyword, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 行政区划查询:适合「郴州市」等城市名 → adcode(须用 UriComponentsBuilder 编码中文查询串) */
|
||||
private String tryDistrict(String keyword, String apiKey) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromHttpUrl(properties.getDistrictUrl())
|
||||
.queryParam("keywords", keyword)
|
||||
.queryParam("subdistrict", "0")
|
||||
.queryParam("key", apiKey)
|
||||
.build()
|
||||
.toUriString();
|
||||
String body = restTemplate.getForObject(url, String.class);
|
||||
JSONObject root = body == null ? new JSONObject() : JSONObject.parseObject(body);
|
||||
if (!isAmapHttpSuccess(root)) {
|
||||
log.debug("[amap] district 未成功 keyword={} info={}", keyword, root.getString("info"));
|
||||
return null;
|
||||
}
|
||||
JSONArray districts = root.getJSONArray("districts");
|
||||
String ad = extractAdcodeFromDistrictTree(districts);
|
||||
if (!StringUtils.hasText(ad) && districts != null && !districts.isEmpty()) {
|
||||
log.warn("[amap] district 返回成功但未解析出 adcode keyword={} districtsSample={}",
|
||||
keyword, districts.size() > 0 ? districts.getJSONObject(0).toJSONString() : "[]");
|
||||
}
|
||||
return ad;
|
||||
} catch (Exception e) {
|
||||
log.warn("[amap] district 异常 keyword={}", keyword, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入提示:在行政区划仍失败时兜底;优先取与关键词完全同名且 adcode 为「市级」形态的条目(末三位常为 000)。
|
||||
*/
|
||||
private String tryInputTipsCityAdcode(String keyword, String apiKey) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromHttpUrl("https://restapi.amap.com/v3/assistant/inputtips")
|
||||
.queryParam("keywords", keyword)
|
||||
.queryParam("datatype", "all")
|
||||
.queryParam("key", apiKey)
|
||||
.build()
|
||||
.toUriString();
|
||||
String body = restTemplate.getForObject(url, String.class);
|
||||
JSONObject root = body == null ? new JSONObject() : JSONObject.parseObject(body);
|
||||
if (!isAmapHttpSuccess(root)) {
|
||||
return null;
|
||||
}
|
||||
JSONArray tips = root.getJSONArray("tips");
|
||||
if (tips == null || tips.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < tips.size(); i++) {
|
||||
JSONObject t = tips.getJSONObject(i);
|
||||
if (!keyword.equals(t.getString("name"))) {
|
||||
continue;
|
||||
}
|
||||
String ad = t.getString("adcode");
|
||||
if (!StringUtils.hasText(ad) || ad.length() != 6) {
|
||||
continue;
|
||||
}
|
||||
if (ad.endsWith("000")) {
|
||||
return ad;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < tips.size(); i++) {
|
||||
JSONObject t = tips.getJSONObject(i);
|
||||
if (!keyword.equals(t.getString("name"))) {
|
||||
continue;
|
||||
}
|
||||
String ad = t.getString("adcode");
|
||||
if (StringUtils.hasText(ad) && ad.length() == 6) {
|
||||
return ad;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.debug("[amap] inputtips 异常 keyword={}", keyword, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 高德 Web 服务 status 可能为字符串 "1" 或数字 1 */
|
||||
private static boolean isAmapHttpSuccess(JSONObject root) {
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
Object s = root.get("status");
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
if (s instanceof Number) {
|
||||
return ((Number) s).intValue() == 1;
|
||||
}
|
||||
return "1".equals(String.valueOf(s).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从行政区划树取 6 位 adcode:优先市级,再区县、省;跳过全国 100000。
|
||||
*/
|
||||
private static String extractAdcodeFromDistrictTree(JSONArray districts) {
|
||||
if (districts == null || districts.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String ad = pickAdcodeByLevel(districts, "city");
|
||||
if (StringUtils.hasText(ad)) {
|
||||
return ad;
|
||||
}
|
||||
ad = pickAdcodeByLevel(districts, "district");
|
||||
if (StringUtils.hasText(ad)) {
|
||||
return ad;
|
||||
}
|
||||
for (int i = 0; i < districts.size(); i++) {
|
||||
JSONArray sub = districts.getJSONObject(i).getJSONArray("districts");
|
||||
ad = extractAdcodeFromDistrictTree(sub);
|
||||
if (StringUtils.hasText(ad)) {
|
||||
return ad;
|
||||
}
|
||||
}
|
||||
ad = pickAdcodeByLevel(districts, "province");
|
||||
if (StringUtils.hasText(ad)) {
|
||||
return ad;
|
||||
}
|
||||
for (int i = 0; i < districts.size(); i++) {
|
||||
String a = districts.getJSONObject(i).getString("adcode");
|
||||
if (StringUtils.hasText(a) && a.length() == 6 && !"100000".equals(a)) {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String pickAdcodeByLevel(JSONArray districts, String wantLevel) {
|
||||
if (districts == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < districts.size(); i++) {
|
||||
JSONObject d = districts.getJSONObject(i);
|
||||
String level = d.getString("level");
|
||||
if (level == null || !wantLevel.equalsIgnoreCase(level.trim())) {
|
||||
continue;
|
||||
}
|
||||
String ad = d.getString("adcode");
|
||||
if (StringUtils.hasText(ad) && ad.length() == 6 && !"100000".equals(ad)) {
|
||||
return ad;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JSONObject requestWeather(String adcode, String apiKey, String extensions) {
|
||||
String url = UriComponentsBuilder.fromHttpUrl(properties.getWeatherUrl())
|
||||
.queryParam("city", adcode)
|
||||
.queryParam("key", apiKey)
|
||||
.queryParam("extensions", extensions)
|
||||
.build()
|
||||
.toUriString();
|
||||
String body = restTemplate.getForObject(url, String.class);
|
||||
JSONObject root = body == null ? new JSONObject() : JSONObject.parseObject(body);
|
||||
if (!isAmapHttpSuccess(root)) {
|
||||
throw new IllegalStateException("高德天气接口失败: " + root.getString("info"));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private WeatherAggregateRespVO buildVo(JSONObject baseRoot, JSONObject allRoot) {
|
||||
WeatherAggregateRespVO vo = new WeatherAggregateRespVO();
|
||||
|
||||
JSONArray lives = baseRoot.getJSONArray("lives");
|
||||
if (lives != null && !lives.isEmpty()) {
|
||||
JSONObject live = lives.getJSONObject(0);
|
||||
vo.setCityLabel(firstNonBlank(live.getString("city"), live.getString("province"), "—"));
|
||||
WeatherAggregateRespVO.Current c = new WeatherAggregateRespVO.Current();
|
||||
c.setTemp(live.getString("temperature"));
|
||||
c.setDesc(live.getString("weather"));
|
||||
c.setIcon(AmapWeatherIconMapper.iconUrlFromDesc(live.getString("weather"),
|
||||
properties.getWeatherIconUrlPattern()));
|
||||
vo.setCurrent(c);
|
||||
}
|
||||
|
||||
JSONArray forecasts = allRoot.getJSONArray("forecasts");
|
||||
if (forecasts == null || forecasts.isEmpty()) {
|
||||
vo.setDays(Collections.emptyList());
|
||||
if (vo.getCityLabel() == null) {
|
||||
vo.setCityLabel("—");
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
JSONObject firstFc = forecasts.getJSONObject(0);
|
||||
if (vo.getCityLabel() == null || vo.getCityLabel().isEmpty()) {
|
||||
vo.setCityLabel(firstNonBlank(firstFc.getString("city"), firstFc.getString("province"), "—"));
|
||||
}
|
||||
|
||||
JSONArray casts = firstFc.getJSONArray("casts");
|
||||
if (casts == null || casts.isEmpty()) {
|
||||
vo.setDays(Collections.emptyList());
|
||||
return vo;
|
||||
}
|
||||
|
||||
DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
DateTimeFormatter md = DateTimeFormatter.ofPattern("M/d");
|
||||
List<WeatherAggregateRespVO.Day> days = new ArrayList<>();
|
||||
int n = Math.min(3, casts.size());
|
||||
for (int i = 0; i < n; i++) {
|
||||
JSONObject cast = casts.getJSONObject(i);
|
||||
WeatherAggregateRespVO.Day day = new WeatherAggregateRespVO.Day();
|
||||
String dateStr = cast.getString("date");
|
||||
day.setFxDate(dateStr);
|
||||
if (StringUtils.hasText(dateStr)) {
|
||||
try {
|
||||
LocalDate ld = LocalDate.parse(dateStr, iso);
|
||||
int dow = ld.getDayOfWeek().getValue();
|
||||
String[] wz = {"一", "二", "三", "四", "五", "六", "日"};
|
||||
day.setWeekLabel("周" + wz[dow - 1]);
|
||||
day.setDateLabel(ld.format(md));
|
||||
} catch (Exception e) {
|
||||
day.setWeekLabel(cast.getString("week"));
|
||||
day.setDateLabel(dateStr);
|
||||
}
|
||||
} else {
|
||||
day.setWeekLabel(cast.getString("week"));
|
||||
day.setDateLabel("");
|
||||
}
|
||||
String daytemp = cast.getString("daytemp");
|
||||
String nighttemp = cast.getString("nighttemp");
|
||||
day.setRange(formatRange(nighttemp, daytemp));
|
||||
day.setDesc(firstNonBlank(cast.getString("dayweather"), cast.getString("nightweather"), "—"));
|
||||
day.setIcon(AmapWeatherIconMapper.iconUrlFromDesc(cast.getString("dayweather"),
|
||||
properties.getWeatherIconUrlPattern()));
|
||||
days.add(day);
|
||||
}
|
||||
vo.setDays(days);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static String formatRange(String nighttemp, String daytemp) {
|
||||
try {
|
||||
int d = Integer.parseInt(daytemp.trim());
|
||||
int n = Integer.parseInt(nighttemp.trim());
|
||||
int min = Math.min(d, n);
|
||||
int max = Math.max(d, n);
|
||||
return min + "~" + max + "°C";
|
||||
} catch (Exception e) {
|
||||
if (StringUtils.hasText(daytemp) && StringUtils.hasText(nighttemp)) {
|
||||
return nighttemp + "~" + daytemp + "°C";
|
||||
}
|
||||
return "—";
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String... ss) {
|
||||
if (ss == null) {
|
||||
return "—";
|
||||
}
|
||||
for (String s : ss) {
|
||||
if (StringUtils.hasText(s)) {
|
||||
return s.trim();
|
||||
}
|
||||
}
|
||||
return "—";
|
||||
}
|
||||
|
||||
private static String trimKey(String key) {
|
||||
return key == null ? "" : key.trim();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user