完善论坛首页以及天气接口对接
This commit is contained in:
parent
ca442c7e67
commit
1ae63df5de
@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -12,6 +13,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class WebConfiguration implements WebMvcConfigurer {
|
public class WebConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate(){
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder(){
|
public PasswordEncoder passwordEncoder(){
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.example.controller;
|
||||||
|
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import com.example.entity.vo.response.WeatherVO;
|
||||||
|
import com.example.service.WeatherService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
//论坛相关接口都在这里
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/forum")
|
||||||
|
public class ForumController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
WeatherService service;
|
||||||
|
|
||||||
|
@GetMapping("/weather")
|
||||||
|
public RestBean<WeatherVO> weather(double latitude, double longitude){
|
||||||
|
WeatherVO weatherVO = service.fetchWeather(latitude, longitude);
|
||||||
|
return weatherVO != null ?
|
||||||
|
RestBean.success(weatherVO) : RestBean.failure(400, "获取地理位置信息失败!");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.entity.vo.response;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WeatherVO {
|
||||||
|
JSONObject now;
|
||||||
|
JSONObject location;
|
||||||
|
JSONArray hourly;
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.example.service;
|
||||||
|
|
||||||
|
import com.example.entity.vo.response.WeatherVO;
|
||||||
|
|
||||||
|
public interface WeatherService {
|
||||||
|
WeatherVO fetchWeather(double latitude, double longitude);
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.example.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.example.entity.vo.response.WeatherVO;
|
||||||
|
import com.example.service.WeatherService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class WeatherServiceImpl implements WeatherService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate template;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
RestTemplate rest;
|
||||||
|
|
||||||
|
@Value("${spring.weather.key}")
|
||||||
|
String key;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WeatherVO fetchWeather(double latitude, double longitude) {
|
||||||
|
return fetchFromCache(latitude, longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WeatherVO fetchFromCache(double latitude, double longitude) {
|
||||||
|
JSONObject geo = this.decompressStringToJSON(rest.getForObject(
|
||||||
|
"https://geoapi.qweather.com/v2/city/lookup?location="+longitude+","+latitude+"&key="+key, byte[].class));
|
||||||
|
if(geo == null) return null;
|
||||||
|
JSONObject location = geo.getJSONArray("location").getJSONObject(0);
|
||||||
|
int id = location.getInteger("id");
|
||||||
|
String key = "weather:"+id;
|
||||||
|
String cache = template.opsForValue().get(key);
|
||||||
|
if(cache != null)
|
||||||
|
return JSONObject.parseObject(cache).to(WeatherVO.class);
|
||||||
|
WeatherVO weather = this.fetchFromAPI(id, location);
|
||||||
|
if(weather == null)
|
||||||
|
return null;
|
||||||
|
template.opsForValue().set(key, JSONObject.from(weather).toJSONString(), 1, TimeUnit.HOURS);
|
||||||
|
return weather;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WeatherVO fetchFromAPI(int id, JSONObject location) {
|
||||||
|
WeatherVO vo = new WeatherVO();
|
||||||
|
vo.setLocation(location);
|
||||||
|
JSONObject now = this.decompressStringToJSON(rest.getForObject(
|
||||||
|
"https://devapi.qweather.com/v7/weather/now?location="+id+"&key="+key, byte[].class));
|
||||||
|
if(now == null) return null;
|
||||||
|
vo.setNow(now.getJSONObject("now"));
|
||||||
|
JSONObject hourly = this.decompressStringToJSON(rest.getForObject(
|
||||||
|
"https://devapi.qweather.com/v7/weather/24h?location="+id+"&key="+key, byte[].class));
|
||||||
|
if(hourly == null) return null;
|
||||||
|
vo.setHourly(new JSONArray(hourly.getJSONArray("hourly").stream().limit(5).toList()));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject decompressStringToJSON(byte[] data) {
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(data));
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = gis.read(buffer)) != -1)
|
||||||
|
stream.write(buffer, 0, bytesRead);
|
||||||
|
gis.close();
|
||||||
|
stream.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return JSONObject.parseObject(stream.toString());
|
||||||
|
}
|
||||||
|
}
|
@ -43,3 +43,5 @@ spring:
|
|||||||
endpoint: 'http://localhost:9000'
|
endpoint: 'http://localhost:9000'
|
||||||
username: 'minio'
|
username: 'minio'
|
||||||
password: 'password'
|
password: 'password'
|
||||||
|
weather:
|
||||||
|
key: 7abbc24d3b4443b597a3b3c676e0f221
|
||||||
|
@ -47,3 +47,5 @@ spring:
|
|||||||
endpoint: 'http://localhost:9000'
|
endpoint: 'http://localhost:9000'
|
||||||
username: 'minio'
|
username: 'minio'
|
||||||
password: 'password'
|
password: 'password'
|
||||||
|
weather:
|
||||||
|
key: 7abbc24d3b4443b597a3b3c676e0f221
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI",
|
||||||
|
Roboto, "Helvetica Neue", Arial, "Noto Sans",
|
||||||
|
"Liberation Sans", sans-serif, "Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,59 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const data = {
|
defineProps({
|
||||||
location: '中国 四川省',
|
data: Object
|
||||||
city: '成都市',
|
})
|
||||||
temperature: 22,
|
|
||||||
icon: '101',
|
|
||||||
text: '多云',
|
|
||||||
hourly: [
|
|
||||||
{
|
|
||||||
icon: '100',
|
|
||||||
temperature: 22,
|
|
||||||
time: '1时'
|
|
||||||
}, {
|
|
||||||
icon: '102',
|
|
||||||
temperature: 30,
|
|
||||||
time: '2时'
|
|
||||||
}, {
|
|
||||||
icon: '101',
|
|
||||||
temperature: 21,
|
|
||||||
time: '3时'
|
|
||||||
}, {
|
|
||||||
icon: '103',
|
|
||||||
temperature: 25,
|
|
||||||
time: '4时'
|
|
||||||
}, {
|
|
||||||
icon: '104',
|
|
||||||
temperature: 24,
|
|
||||||
time: '5时'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="height: 160px" v-loading="!data.success"
|
||||||
<div style="display: flex;justify-content: space-between;margin: 10px 20px">
|
element-loading-text="正在加载天气信息...">
|
||||||
|
<div style="display: flex;justify-content: space-between;margin: 10px 20px"
|
||||||
|
v-if="data.success">
|
||||||
<div style="font-size: 45px">
|
<div style="font-size: 45px">
|
||||||
<i :class="`qi-${data.icon}-fill`"></i>
|
<i :class="`qi-${data.now.icon}-fill`"></i>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight: bold;text-align: center">
|
<div style="font-weight: bold;text-align: center">
|
||||||
<div style="font-size: 25px">{{data.temperature}}℃</div>
|
<div style="font-size: 25px">{{data.now.temp}}℃</div>
|
||||||
<div style="font-size: 15px">{{data.text}}</div>
|
<div style="font-size: 15px">{{data.now.text}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 13px">
|
<div style="margin-top: 13px">
|
||||||
<div style="font-size: 15px">{{data.city}}</div>
|
<div style="font-size: 15px">{{data.location.country + ' ' + data.location.adm1}}</div>
|
||||||
<div style="font-size: 14px;color: grey">{{data.location}}</div>
|
<div style="font-size: 14px;color: grey">{{ `${data.location.adm2} ${data.location.name}` }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-divider style="margin: 10px 0"/>
|
<el-divider style="margin: 10px 0"/>
|
||||||
<div style="display: grid;grid-template-columns: repeat(5, 1fr);text-align: center">
|
<div style="display: grid;grid-template-columns: repeat(5, 1fr);text-align: center">
|
||||||
<div v-for="item in data.hourly">
|
<div v-for="item in data.hourly">
|
||||||
<div style="font-size: 13px">{{item.time}}</div>
|
<div style="font-size: 13px">{{new Date(item.fxTime).getHours() + ' 时'}}</div>
|
||||||
<div style="font-size: 23px">
|
<div style="font-size: 23px">
|
||||||
<i :class="`qi-${item.icon}-fill`"></i>
|
<i :class="`qi-${item.icon}-fill`"></i>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 12px">{{item.temperature}}℃</div>
|
<div style="font-size: 12px">{{item.temp}}℃</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,41 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import LightCard from "@/components/LightCard.vue";
|
import LightCard from "@/components/LightCard.vue";
|
||||||
import {Calendar, CollectionTag, EditPen, Link} from "@element-plus/icons-vue";
|
import {Calendar, CollectionTag, EditPen, Link} from "@element-plus/icons-vue";
|
||||||
import {computed} from "vue"
|
import {computed, reactive} from "vue"
|
||||||
import Weather from "@/components/Weather.vue";
|
import Weather from "@/components/Weather.vue";
|
||||||
|
import {get} from "@/net";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
|
||||||
|
const weather = reactive({
|
||||||
|
now: {},
|
||||||
|
location: {},
|
||||||
|
hourly: [],
|
||||||
|
success: false
|
||||||
|
})
|
||||||
|
|
||||||
const today = computed(() => {
|
const today = computed(() => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
return `${date.getFullYear()} 年 ${date.getMonth()} 月 ${date.getDay()} 日`
|
return `${date.getFullYear()} 年 ${date.getMonth()} 月 ${date.getDay()} 日`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(position => {
|
||||||
|
const latitude = position.coords.latitude
|
||||||
|
const longitude = position.coords.longitude
|
||||||
|
get(`/api/forum/weather?latitude=${latitude}&longitude=${longitude}`, data => {
|
||||||
|
Object.assign(weather, data)
|
||||||
|
weather.success = true
|
||||||
|
})
|
||||||
|
}, error => {
|
||||||
|
console.info(error)
|
||||||
|
ElMessage.warning('获取位置信息超时,请检测浏览器设置')
|
||||||
|
get(`/api/forum/weather?latitude=39.90499&longitude=116.40529`, data => {
|
||||||
|
Object.assign(weather, data)
|
||||||
|
weather.success = true
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
timeout: 3000,
|
||||||
|
enableHighAccuracy: true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -38,7 +66,7 @@ const today = computed(() => {
|
|||||||
天气信息
|
天气信息
|
||||||
</div>
|
</div>
|
||||||
<el-divider style="margin: 10px 0"/>
|
<el-divider style="margin: 10px 0"/>
|
||||||
<weather/>
|
<weather :data="weather"/>
|
||||||
</light-card>
|
</light-card>
|
||||||
<light-card style="margin-top: 10px">
|
<light-card style="margin-top: 10px">
|
||||||
<div style="display: flex;justify-content: space-between;color: grey;font-size: 14px">
|
<div style="display: flex;justify-content: space-between;color: grey;font-size: 14px">
|
||||||
@ -91,4 +119,8 @@ const today = computed(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .edit-topic{
|
||||||
|
background-color: #282828;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user