build: 添加 Maven 项目配置并忽略 IDE 目录。

集成聚合数据的 每日天气、每日一句功能
This commit is contained in:
promise_cyf
2026-10-09 15:58:45 +08:00
parent c24677ab0a
commit 9c04f81f2d
17 changed files with 873 additions and 0 deletions
+2
View File
@@ -43,3 +43,5 @@ buildNumber.properties
# JDT-specific (Eclipse Java Development Tools) # JDT-specific (Eclipse Java Development Tools)
.classpath .classpath
/.codebuddy
/.idea
+3
View File
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
+96
View File
@@ -0,0 +1,96 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.cyf</groupId>
<artifactId>devToolBox</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>devToolBox</name>
<description>在线开发工具箱</description>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<swagger-annotations-v3.version>2.2.22</swagger-annotations-v3.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 本地开发用内存数据库,无需依赖外部 MySQL 即可启动 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations-v3.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
package com.cyf.devToolBox;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.cyf.devToolBox.mapper")
public class DevToolBoxApplication {
public static void main(String[] args) {
SpringApplication.run(DevToolBoxApplication.class, args);
}
}
@@ -0,0 +1,44 @@
package com.cyf.devToolBox.common;
import com.cyf.devToolBox.juhe.exception.JuheApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理,保证异常时也返回统一的 {@link Result} 结构。
*
* @author cyf
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/** 聚合数据接口业务错误(error_code != 0) */
@ExceptionHandler(JuheApiException.class)
public Result<Void> handleJuheApiException(JuheApiException e) {
log.warn("聚合数据接口调用失败:{}", e.getMessage());
return Result.error(e.getErrorCode(), e.getMessage());
}
/** 参数错误 */
@ExceptionHandler(IllegalArgumentException.class)
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("参数校验失败:{}", e.getMessage());
return Result.error(400, e.getMessage());
}
/** 状态错误(如未配置 Key) */
@ExceptionHandler(IllegalStateException.class)
public Result<Void> handleIllegalStateException(IllegalStateException e) {
log.warn("状态校验失败:{}", e.getMessage());
return Result.error(500, e.getMessage());
}
/** 兜底 */
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "系统异常:" + e.getMessage());
}
}
@@ -0,0 +1,62 @@
package com.cyf.devToolBox.common;
import lombok.Data;
/**
* 统一 API 返回结构,所有对外接口都返回该格式。
*
* <pre>
* {
* "code": 0,
* "message": "success",
* "data": { ... }
* }
* </pre>
*
* @param <T> 业务数据类型
* @author cyf
*/
@Data
public class Result<T> {
/** 业务状态码,0 表示成功,非 0 表示失败 */
private int code;
/** 提示信息 */
private String message;
/** 业务数据 */
private T data;
/**
* 成功(带数据)。
*/
public static <T> Result<T> success(T data) {
return build(0, "success", data);
}
/**
* 成功(无数据)。
*/
public static <T> Result<T> success() {
return build(0, "success", null);
}
/**
* 失败。
*
* @param code 业务状态码
* @param message 错误信息
*/
public static <T> Result<T> error(int code, String message) {
return build(code, message, null);
}
private static <T> Result<T> build(int code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
@@ -0,0 +1,80 @@
package com.cyf.devToolBox.controller;
import com.cyf.devToolBox.juhe.soup.JuheSoupClient;
import com.cyf.devToolBox.juhe.soup.model.JuheSoupResponse;
import com.cyf.devToolBox.juhe.weather.JuheWeatherClient;
import com.cyf.devToolBox.juhe.weather.model.JuheWeatherResponse;
import com.cyf.devToolBox.common.Result;
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;
/**
* 聚合数据接口,统一返回 {@link Result} 结构。
*
* <pre>
* GET /api/juhe/weather?city=丰台 查询实时天气(未来天数取配置默认值)
* GET /api/juhe/weather?city=北京&futureNum=3 指定未来天气天数
* GET /api/juhe/weather/raw?city=北京 返回原始 JSON,便于排查
* GET /api/juhe/soup 查询每日一句
* GET /api/juhe/soup/raw 返回原始 JSON,便于排查
* </pre>
*
* @author cyf
*/
@RestController
@RequestMapping("/api/juhe")
public class JuheController {
private final JuheWeatherClient juheWeatherClient;
private final JuheSoupClient juheSoupClient;
public JuheController(JuheWeatherClient juheWeatherClient, JuheSoupClient juheSoupClient) {
this.juheWeatherClient = juheWeatherClient;
this.juheSoupClient = juheSoupClient;
}
/**
* 查询指定城市的天气。
*
* @param city 城市名称(中文),如:北京、丰台
* @param futureNum 未来天气天数(1~7),不传则使用配置中的默认值
* @return 统一返回结构,data 为天气数据
*/
@GetMapping("/weather")
public Result<JuheWeatherResponse.Result> weather(@RequestParam String city,
@RequestParam(required = false) Integer futureNum) {
JuheWeatherResponse.Result data = futureNum == null
? juheWeatherClient.query(city)
: juheWeatherClient.query(city, futureNum);
return Result.success(data);
}
/**
* 天气接口原始 JSON,便于排查问题。
*/
@GetMapping("/weather/raw")
public Result<String> weatherRaw(@RequestParam String city,
@RequestParam(required = false) Integer futureNum) {
return Result.success(juheWeatherClient.queryRaw(city, futureNum));
}
/**
* 查询每日一句。
*
* @return 统一返回结构,data 为每日一句数据
*/
@GetMapping("/soup")
public Result<JuheSoupResponse.Result> soup() {
return Result.success(juheSoupClient.query());
}
/**
* 每日一句接口原始 JSON,便于排查问题。
*/
@GetMapping("/soup/raw")
public Result<String> soupRaw() {
return Result.success(juheSoupClient.queryRaw());
}
}
@@ -0,0 +1,82 @@
package com.cyf.devToolBox.juhe;
import com.cyf.devToolBox.juhe.exception.JuheApiException;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriBuilder;
import java.net.URI;
import java.util.function.Function;
/**
* 聚合数据客户端抽象基类,抽离各接口的公共能力:
* <ul>
* <li>持有配置与 {@link RestClient}</li>
* <li>统一执行请求并校验 {@code error_code}</li>
* <li>统一校验接口 key 是否已配置</li>
* </ul>
* 子类只需关心「拼装参数」与「提取 result」。
*
* @author cyf
*/
public abstract class AbstractJuheClient {
protected final JuheProperties properties;
protected final RestClient restClient;
protected AbstractJuheClient(JuheProperties properties, ClientHttpRequestFactory requestFactory, String baseUrl) {
this.properties = properties;
this.restClient = RestClient.builder()
.baseUrl(baseUrl)
.requestFactory(requestFactory)
.build();
}
/**
* 统一执行 GET 请求、校验 error_code,成功后返回响应对象。
*
* @param uri 请求地址构造器
* @param responseType 响应类型
* @throws JuheApiException 接口返回内容为空或 error_code != 0
*/
protected <T extends JuheResponse> T execute(Function<UriBuilder, URI> uri, Class<T> responseType) {
T response = restClient.get()
.uri(uri)
.retrieve()
.body(responseType);
if (response == null) {
throw new JuheApiException(-1, "接口返回内容为空");
}
if (!response.isSuccess()) {
throw new JuheApiException(
response.getErrorCode() == null ? -1 : response.getErrorCode(),
response.getReason());
}
return response;
}
/**
* 统一执行 GET 请求并返回原始 JSON,便于排查问题。
*/
protected String executeRaw(Function<UriBuilder, URI> uri) {
return restClient.get()
.uri(uri)
.retrieve()
.body(String.class);
}
/**
* 校验接口 key 是否已配置。
*
* @param key 实际的 key 值
* @param configKey 配置项名称,用于错误提示
*/
protected void checkKeyConfigured(String key, String configKey) {
if (!StringUtils.hasText(key)) {
throw new IllegalStateException("未配置聚合数据 API Key(" + configKey + ")");
}
}
}
@@ -0,0 +1,26 @@
package com.cyf.devToolBox.juhe;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
/**
* 聚合数据客户端公共配置。
*
* @author cyf
*/
@Configuration
public class JuheClientConfig {
/**
* 所有聚合数据客户端共用的请求工厂(统一连接/读取超时)。
*/
@Bean
public ClientHttpRequestFactory juheRequestFactory(JuheProperties properties) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout((int) properties.getConnectTimeout().toMillis());
requestFactory.setReadTimeout((int) properties.getReadTimeout().toMillis());
return requestFactory;
}
}
@@ -0,0 +1,72 @@
package com.cyf.devToolBox.juhe;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 聚合数据(Juhe)相关配置,对应 application.yml 中的 juhe.* 配置项。
*
* <pre>
* juhe:
* connect-timeout: 3s
* read-timeout: 5s
* weather:
* base-url: https://apis.juhe.cn/simpleWeather/query
* key: 天气接口的 key
* future-num: 5
* soup:
* base-url: https://apis.juhe.cn/fapig/soup/query
* key: 每日一句接口的 key
* </pre>
*
* @author cyf
*/
@Data
@Component
@ConfigurationProperties(prefix = "juhe")
public class JuheProperties {
/** 连接超时时间 */
private Duration connectTimeout = Duration.ofSeconds(3);
/** 读取超时时间 */
private Duration readTimeout = Duration.ofSeconds(5);
/** 天气查询配置 */
private Weather weather = new Weather();
/** 每日一句配置 */
private Soup soup = new Soup();
/**
* 天气接口配置。
*/
@Data
public static class Weather {
/** 接口地址 */
private String baseUrl = "https://apis.juhe.cn/simpleWeather/query";
/** 天气接口的 API Key */
private String key;
/** 默认查询的未来天气天数(1~7),为空表示只查询实时天气 */
private Integer futureNum;
}
/**
* 每日一句接口配置。
*/
@Data
public static class Soup {
/** 接口地址 */
private String baseUrl = "https://apis.juhe.cn/fapig/soup/query";
/** 每日一句接口的 API Key */
private String key;
}
}
@@ -0,0 +1,23 @@
package com.cyf.devToolBox.juhe;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* 聚合数据接口响应的统一抽象:所有响应都包含 {@code reason} 和 {@code error_code}。
*
* @author cyf
*/
public interface JuheResponse {
/** 返回说明,如:success、查询成功 */
String getReason();
/** 返回码,0 表示成功 */
Integer getErrorCode();
/** 是否请求成功(error_code == 0) */
@JsonIgnore
default boolean isSuccess() {
return getErrorCode() != null && getErrorCode() == 0;
}
}
@@ -0,0 +1,21 @@
package com.cyf.devToolBox.juhe.exception;
/**
* 聚合数据接口调用异常(业务层错误,error_code != 0)。
*
* @author cyf
*/
public class JuheApiException extends RuntimeException {
/** 聚合数据返回的 error_code */
private final int errorCode;
public JuheApiException(int errorCode, String reason) {
super("聚合数据接口调用失败 [error_code=" + errorCode + "]: " + reason);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
@@ -0,0 +1,44 @@
package com.cyf.devToolBox.juhe.soup;
import com.cyf.devToolBox.juhe.AbstractJuheClient;
import com.cyf.devToolBox.juhe.JuheProperties;
import com.cyf.devToolBox.juhe.soup.model.JuheSoupResponse;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriBuilder;
import java.net.URI;
import java.util.function.Function;
/**
* 聚合数据「每日一句」客户端。
*
* @author cyf
*/
@Component
public class JuheSoupClient extends AbstractJuheClient {
public JuheSoupClient(JuheProperties properties, ClientHttpRequestFactory juheRequestFactory) {
super(properties, juheRequestFactory, properties.getSoup().getBaseUrl());
}
/**
* 查询每日一句。
*/
public JuheSoupResponse.Result query() {
checkKeyConfigured(properties.getSoup().getKey(), "juhe.soup.key");
return execute(uri(), JuheSoupResponse.class).getResult();
}
/**
* 每日一句接口的原始 JSON,方便排查问题。
*/
public String queryRaw() {
checkKeyConfigured(properties.getSoup().getKey(), "juhe.soup.key");
return executeRaw(uri());
}
private Function<UriBuilder, URI> uri() {
return uriBuilder -> uriBuilder.queryParam("key", properties.getSoup().getKey()).build();
}
}
@@ -0,0 +1,45 @@
package com.cyf.devToolBox.juhe.soup.model;
import com.cyf.devToolBox.juhe.JuheResponse;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 聚合数据「每日一句」接口响应模型。
*
* <pre>
* {
* "reason": "success",
* "result": { "text": "无论有多困难,都坚强地抬头挺胸..." },
* "error_code": 0
* }
* </pre>
*
* @author cyf
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class JuheSoupResponse implements JuheResponse {
/** 返回说明,如:success */
private String reason;
/** 返回结果集 */
private Result result;
/** 返回码,0 表示成功 */
@JsonProperty("error_code")
private Integer errorCode;
/**
* 每日一句数据主体。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Result {
/** 每日一句内容 */
private String text;
}
}
@@ -0,0 +1,69 @@
package com.cyf.devToolBox.juhe.weather;
import com.cyf.devToolBox.juhe.AbstractJuheClient;
import com.cyf.devToolBox.juhe.JuheProperties;
import com.cyf.devToolBox.juhe.weather.model.JuheWeatherResponse;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriBuilder;
import java.net.URI;
import java.util.function.Function;
/**
* 聚合数据「天气查询」客户端。
*
* @author cyf
*/
@Component
public class JuheWeatherClient extends AbstractJuheClient {
public JuheWeatherClient(JuheProperties properties, ClientHttpRequestFactory juheRequestFactory) {
super(properties, juheRequestFactory, properties.getWeather().getBaseUrl());
}
/**
* 使用配置中的默认 {@code future-num} 查询指定城市的天气。
*/
public JuheWeatherResponse.Result query(String city) {
return query(city, properties.getWeather().getFutureNum());
}
/**
* 查询指定城市的天气。
*
* @param city 城市名称(中文),如:北京、丰台
* @param futureNum 需要查询的未来天气天数(1~7),为 null 或不大于 0 时不查询
*/
public JuheWeatherResponse.Result query(String city, Integer futureNum) {
validate(city);
return execute(uri(city, futureNum), JuheWeatherResponse.class).getResult();
}
/**
* 天气接口的原始 JSON,方便排查问题。
*/
public String queryRaw(String city, Integer futureNum) {
validate(city);
return executeRaw(uri(city, futureNum));
}
private void validate(String city) {
if (!StringUtils.hasText(city)) {
throw new IllegalArgumentException("城市名称不能为空");
}
checkKeyConfigured(properties.getWeather().getKey(), "juhe.weather.key");
}
private Function<UriBuilder, URI> uri(String city, Integer futureNum) {
return uriBuilder -> {
uriBuilder.queryParam("key", properties.getWeather().getKey());
uriBuilder.queryParam("city", city);
if (futureNum != null && futureNum > 0) {
uriBuilder.queryParam("future_num", futureNum);
}
return uriBuilder.build();
};
}
}
@@ -0,0 +1,123 @@
package com.cyf.devToolBox.juhe.weather.model;
import com.cyf.devToolBox.juhe.JuheResponse;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 聚合数据天气接口的原始响应模型。
*
* <pre>
* {
* "reason": "查询成功",
* "result": {
* "city": "苏州",
* "realtime": { ... },
* "future": [ ... ]
* },
* "error_code": 0
* }
* </pre>
*
* @author cyf
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class JuheWeatherResponse implements JuheResponse {
/** 返回说明,如:查询成功 */
private String reason;
/** 返回结果集 */
private Result result;
/** 返回码,0 表示成功 */
@JsonProperty("error_code")
private Integer errorCode;
/**
* 天气数据主体。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Result {
/** 城市名 */
private String city;
/** 实时天气 */
private Realtime realtime;
/** 未来天气,未查询 future_num 时可能为空 */
private List<Future> future;
}
/**
* 实时天气。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Realtime {
/** 温度(℃) */
private String temperature;
/** 湿度(%) */
private String humidity;
/** 天气现象,如:阴 */
private String info;
/** 天气标识,注意此处是字符串,如:"02" */
private String wid;
/** 风向,如:西北风 */
private String direct;
/** 风力,如:3级 */
private String power;
/** 空气质量指数 */
private String aqi;
}
/**
* 未来某天的天气预报。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Future {
/** 日期,如:2019-02-22 */
private String date;
/** 温度区间,如:1/7℃ */
private String temperature;
/** 天气,如:小雨转多云 */
private String weather;
/** 天气标识,注意此处是对象(day / night) */
private Wid wid;
/** 风向,如:北风转西北风 */
private String direct;
}
/**
* 未来天气的 wid 结构,仅 future 中是对象,与 realtime.wid 的字符串类型不同。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Wid {
/** 白天天气标识 */
private String day;
/** 夜间天气标识 */
private String night;
}
}
+66
View File
@@ -0,0 +1,66 @@
server:
port: 8077
spring:
application:
name: dev-toolbox
# 默认使用 H2 内存库,本地无需任何外部依赖即可启动
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:dev_toolbox;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
username: sa
password: ""
h2:
console:
enabled: true
path: /h2-console
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
banner: false
db-config:
id-type: auto
# 聚合数据(每个接口使用各自的 key)
# 天气文档:https://www.juhe.cn/docs/api/id/73
# 每日一句文档:https://www.juhe.cn/docs/api/id/86
juhe:
connect-timeout: 3s
read-timeout: 5s
# 天气查询
weather:
base-url: https://apis.juhe.cn/simpleWeather/query
# 建议改用环境变量 JUHE_WEATHER_KEY 覆盖,避免明文提交到仓库
key: ${JUHE_WEATHER_KEY:ca38eda68ea7242a3601e4e98afa8499}
# 查询未来天气天数(1~7),不填写则只返回实时天气
future-num: 5
# 每日一句
soup:
base-url: https://apis.juhe.cn/fapig/soup/query
# 每日一句接口的 key,请填入并建议用环境变量 JUHE_SOUP_KEY 覆盖
key: ${JUHE_SOUP_KEY:cf8695f97027c22b346a468697ba11db}
---
# 切换为 MySQL 时使用该 profile:mvn spring-boot:run -Dspring-boot.run.profiles=mysql
spring:
config:
activate:
on-profile: mysql
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dev_toolbox?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
logging:
level:
com.cyf.devToolBox: debug