如果你想开发一个应用(1-20)

##天气api##
上一章里我们已经可以手动设置天气情况,但在一般情况下,天气情况都是客观的,所以他不应该由人手动设置。所以读取天气接口自动获取就是一个必须的功能点了。

天气预报的接口有很多,最早的weather.cn有时好时坏,所以最终选择了心知天气的接口。

这个接口的免费版可以支持国内市级的几乎所有城市,这也是我在上一章把选择地区的精确度定为市级的原因之一。并且可以根据名称和坐标等功能获取实时天气,当前阶段,免费版也可以支持现有的功能.

心知天气的用法很简单,首先注册一个账号,然后就回有一个key,接下来将key嵌入到url中就可以通过webapi的方式get回一个json的字符串,解析即可。

##key的保存方式##
为了未来程序的扩展性和保密性,天气api的key不可以写在代码内,可以选择保存在配置文件中或者创立一个字典表。经过多方便考虑,我选择使用字典表的方式来进行保存,首先在数据库中创建字典表,然后在程序中,他所对应的数据模型如下:

@Entity(name = "dictionaryitems")
public class DictionaryItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;               
    private Integer sort;             //排序
    private String dicname;            //字典key
    private String dicvalue;        //字典值
    private String typevalue;        //字典值类型(多项 分组用)
    //getter  setter
}

有了之前的框架,接下来的代码就比较容易了,还是一样的,持久层jpa接口:

public interface DictionaryItemRepository extends JpaRepository<DictionaryItem,Integer> {
    List<DictionaryItem> findByDicname(String dicName);  //根据字典key获取值
    List<DictionaryItem> findByTypevalue(String typeValue);  //根据类型获取值
    List<DictionaryItem> findByTypevalueOrderBySort(String typeValue);//根据类型获取值并排序
}

其实在当前,我们需要使用的只有第一个。

需要注意,在当前不考虑做系统后台的情况下,此字典表均需手动录入,也就是或只有jpa层,不需要服务层

##天气数据模型##
现在假设你已经注册完成,并且进入面试使用api的界面,可以看到若干接口,因为属于欠高端用户,所以我们只看接口名后边没有付费接口字样的接口。经过查询,很容易就找到我们需要的:
逐日天气预报和昨日天气,可以看到接口路径为/weather/daily.json

接口文档及参数:

key
你的API密钥
location
所查询的位置
参数值范围:

城市ID 例如:location=WX4FBXXFKE4F
城市中文名 例如:location=北京
省市名称组合 例如:location=辽宁朝阳、location=北京朝阳
城市拼音/英文名 例如:location=beijing(如拼音相同城市,可在之前加省份和空格,例:shanxi yulin)
经纬度 例如:location=39.93:116.40(格式是 纬度:经度,英文冒号分隔)
IP地址 例如:location=220.181.111.86(某些IP地址可能无法定位到城市)
“ip”两个字母 自动识别请求IP地址,例如:location=ip
language
语言 (可选)

unit
单位 (可选)
参数值范围:

c 当参数为c时,温度c、风速km/h、能见度km、气压mb
f 当参数为f时,温度f、风速mph、能见度mile、气压inch
默认值:c

start
起始时间 (可选)
参数值范围:

日期 例如:start=2015/10/1
整数 例如:start=-2 代表前天、start=-1 代表昨天、start=0 代表今天、start=1 代表明天
默认值:0

days
天数 (可选) 返回从start算起days天的结果。默认为你的权限允许的最多天数。

经过筛选,我们可以看到:

key:自己当前的key
location:前端定位或选择的省市级组合单位
language:zh-Hans
unit:c
start:0(今天)
days:1(不需要预报功能,只是实时查询今天天气)

ok,假设选择了北京,最终的参数查询url为:

https://api.seniverse.com/v3/weather/daily.json?key=mykey&location=北京北京&language=zh-Hans&unit=c&start=0&days=1

返回的查询结果为(已手动格式化):

{
    "results":[
        {
            "location":{
                "id":"mykey",
                "name":"北京",
                "country":"CN",
                "path":"北京,北京,中国",
                "timezone":"Asia/Shanghai",
                "timezone_offset":"+08:00"
            },
            "daily":[
                {
                    "date":"2018-02-11",
                    "text_day":"晴",
                    "code_day":"0",
                    "text_night":"晴",
                    "code_night":"1",
                    "high":"0",
                    "low":"-8",
                    "precip":"",
                    "wind_direction":"西北",
                    "wind_direction_degree":"315",
                    "wind_speed":"20",
                    "wind_scale":"4"
                }
            ],
            "last_update":"2018-02-11T18:00:00+08:00"
        }
    ]
}

下面看看,在这些属性中我们需要的和不需要的,貌似除了时区和最后更新时间外,均需要可以保存,所以最终数据模型为:

@Entity(name = "weather")
public class Weather {
    private Integer id;
    private String name;
    private String path;
    private String weatherdate;
    private String text_day;
    private Integer code_day;
    private Integer temp_high;
    private Integer temp_low;
    private String  precip;
    private String wind_direction;
    private String wind_direction_degree;
    private String wind_speed;
    private String wind_scale;
    private Integer isweb;
    setter... getter...
}

其中isweb的属性用来确认是网络获取还是本地设置。

##网络访问##

由于金钱的原因,现有账户每小时只能访问400次,所以需要必要的缓存机制缓存到本地,这样就不能由客户端直接访问心知天气的api,只能由服务器端缓存后在发送至客户端。这样,就需要java端进行必须的服务器访问操作。

按照RESTful的思想,访问的都是资源,也就是可以把它理解为一个网络数据库,所以同样,创建一个包用来存放web持久层,当然这里没有jpa了,只能够自己写实现.同时,想到之后可能会有切换天气api的需求,所以将逻辑封装到实现内,这里只返回一个weather对象:

public interface WeatherWebData {
    String serviceUrl="https://api.seniverse.com/v3/weather/daily.json?key=%s&location=%s&language=zh-Hans&unit=c&start=0&days=1";
    public Weather getWeatherByLocation(String weatherKey, String location);
}

将配置好的链接参数保存在接口内。

对于网络资源的访问选择了apache的http组件,所以同意需要使用Maven进行引入:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.5</version>
</dependency>

然后在实现中完成对接口的访问:

@Repository
public class WeatherWebDataImpl implements WeatherWebData {
    public Weather getWeatherByLocation(String weatherKey, String location) {
    try {
        HttpClient client =  new DefaultHttpClient();
        HttpUriRequest request=new HttpGet(String.format(this.serviceUrl,weatherKey,location));
        request.setHeader("Content-type","application/json;charset=utf-8");
        HttpResponse response= client.execute(request);
        String result = EntityUtils.toString(response.getEntity(), "utf-8");
        System.out.println(result);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;

}

这段代码天生就适合进行提取方法的重构,所以在工具包内创建一个HttpUtil类,首先封装一下最简单get访问形式,返回String即可:

public class HttpUtil {
    public static String get(String url){
        HttpClient client =  new DefaultHttpClient();
        HttpUriRequest request=new HttpGet(url);
        request.setHeader("Content-type","application/json;charset=utf-8");
        HttpResponse response= null;
        String result = null;
        try {
            response = client.execute(request);
            result = EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
}

此时先不考虑异常情况,实际情况下异常需前端配合,直接显示手动天气设置按钮。

然后在接口实现里替换掉即可:

String url=String.format(this.serviceUrl,weatherKey,location);
   String result= HttpUtil.get(url);

在进行对象创建之前,还要先看一下请求成功之外的情况,把随便给个非法的参数,比如用户key为空,看看返回情况:

{
    "status":"The API key is invalid.",
    "status_code":"AP010003"
}

格式不一致就好办了,可以通过判断status来判断返回的成功或者失败。

##JSON解析##

由于Weather的转换不具有普遍性,所以就不创建共有的工具类,在实现类中通过私有类来实现,String到对象的转换有很多种方法,比如之前刚刚用过的jackson,但这里由于实体类和json对象的属性并没有一一对应,所以jackson就不那么特别适合。
那么有没有其他方法呢,答案当然是肯定的,这里使用阿里出的fastjson,还是一样的,通过maven进行引入:

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.46</version>
</dependency>

他的使用很简单,就好像是mybatis一样,将一个对象以Map或List>的形式返回,这样,只要我们知道json的结构,就可以轻而易举的将它转换为任何形式的对象,这里即没啥好说了,直接贴方法代码:

 private Weather jsonToWeather(String json){
    Weather weather=new Weather();
    Map<String,Object> map = JSON.parseObject(json);
    //判断失败
    if(!map.containsKey("status")) {
        //正常情况
        //weather是result节点的第一项
        Map<String,Object> weatherMap= ((List<Map<String,Object>>)map.get("results")).get(0);
        Map<String, Object> locationJson = (Map<String, Object>) weatherMap.get("location");
        weather.setName(locationJson.get("name").toString());
        weather.setPath(locationJson.get("path").toString());
        Map<String, Object> dailyJson = ((List<Map<String, Object>>) weatherMap.get("daily")).get(0);
        weather.setWeatherdate(dailyJson.get("date").toString());
        weather.setCode_day(Integer.parseInt(dailyJson.get("code_day").toString()));
        weather.setText_day(dailyJson.get("text_day").toString());
        weather.setTemp_high(Integer.parseInt(dailyJson.get("high").toString()));
        weather.setTemp_low(Integer.parseInt(dailyJson.get("low").toString()));
        weather.setWind_direction(dailyJson.get("wind_direction").toString());
        weather.setWind_direction_degree(dailyJson.get("wind_direction_degree").toString());
        weather.setWind_scale(dailyJson.get("wind_scale").toString());
        weather.setWind_speed(dailyJson.get("wind_speed").toString());
        weather.setPrecip(dailyJson.get("precip").toString());
        weather.setIsweb(1);
        return weather;
    }
    return null;
}

最终完成实现方法:

@Repository
public class WeatherWebDataImpl implements WeatherWebData {
    public Weather getWeatherByLocation(String weatherKey, String location) {
        String url=String.format(this.serviceUrl,weatherKey,location);
        String result= HttpUtil.get(url);
        return jsonToWeather(result);
    }
    private Weather jsonToWeather(String json){
    ......
    }
}

##服务层代码##

接下来是服务层,这层就没啥好说的了,接口定义了一个方法,通过地址查询天气:

public interface WeatherService {
    public Object weather(String address);
}

然后实现稍微复杂一些,来统计一些实现需完成的操作:

  1. 查询缓存内是否已有今天此地的天气,如有直接返回
  2. 通过字典表查询心知天气的api所需key
  3. 调用天气资源,查询此地天气
  4. 将返回天气存入db
  5. 返回天气

接下来就一步一步完成这个服务层:

@Service
public class WeatherServiceImpl implements WeatherService{
    public Weather weather(String address) {
        return null;
    }
}

###查询缓存内是否已有今天此地的天气,如有直接返回###

注入天气持久层,并根据日期进行查询:

@Autowired
private WeatherRepository weatherRepository;

.....
public Weather weather(String address) {
     Weather weather=getWeatherByDb(address,(new SimpleDateFormat("yyyy-MM-dd")).format(new Date()));
    return weather;
}

###通过字典表查询心知天气的api所需key###

首先还是引入字典持久层,然后封装一个查询key的私有方法(后期可能改为工具类),并放入缓存(暂时使用静态字段代替,后期使用Spring-Cache框架管理):

@Autowired
private DictionaryItemRepository dictionaryItemRepository;

private static String weatherKey="";
private String getWeatherKey(){
    //缓存为空
    if(WeatherServiceImpl.weatherKey.equals("")){
        //查询字典表
        List<DictionaryItem> dicList=dictionaryItemRepository.findByDicname("weatherKey");
        if(dicList.size()>0)
            weatherKey= dicList.get(0).getDicvalue();
    }
    return weatherKey;
}

##调用天气资源,查询此地天气##

注入之前封装好的网络持久层,并继续增量代码:

@Autowired
private WeatherWebData weatherWebData;
...

public Weather weather(String address) {
    ...
    if(weather==null){
        //如果没有,则查询,并存储到db 返回新内容
        weather= weatherWebData.getWeatherByLocation(getWeatherKey(),address);
    }
}

###将返回天气存入db###

同样封装一个天气存储的方法,保存的同时还可获取db的自增ID:

private Weather saveWeather(Weather weather){
    return weatherRepository.saveAndFlush(weather);
}

最终,返回天气(接口方法完整代码):

public Weather weather(String address) {
    //查询db中是否有此日此地天气
    Weather weather=getWeatherByDb(address,(new SimpleDateFormat("yyyy-MM-dd")).format(new Date()));
    if(weather==null){
        //如果没有,则查询,并存储到db 返回新内容
        weather= weatherWebData.getWeatherByLocation(getWeatherKey(),address);
        weather =  saveWeather(weather);
    }
    return weather;

}

##控制器##

由于操作均封装到了服务层,所以控制器已经尽可能的薄了:

@RequestMapping(value = "/api/weather",method = RequestMethod.POST)
public Object getWeather(HttpServletRequest request,@RequestBody Map map){
    return result(weatherService.weather(map.get("address").toString()));
}

##前端逻辑修改##

后端折腾了一条线,终于要修改前端了,其实前端相对来说修改的地方很少。

由于没有真机测试,所以现在只完成手动设置地点后天气获取

继续进入CreateOrShowDiaryItem.vue组件,修改设置地区的关闭按钮事件:

addressClose:function(event){
    this.adddialog=false;
    //查询此地的天气 省市组合
    this.searchWeather( this.addressProvince+""+this.addressCity);
},

使用searchWeather方法进行服务器端查询:

searchWeather:function(address){
    var data={
        address:address
    };
    this.$http.post("/api/weather",data,{headers:{"token":this.token}}).then(res=>{
        if(res.data.msg!=""){
            //使用手动天气设置
            this.$store.commit('setWeatherIsShow',true);
        }
        var result=res.data.data;
        if(!(result== undefined ||result=="")){
            //关闭手动设置按钮
            this.$store.commit('setWeatherIsShow',false);
            this.weatherContent=result;
            this.weatherText= result.text_day+" "+result.temp_high+"度/"+result.temp_low+"度";
        }

    },res=>{
        //查询服务器失败,同样显示天气设定界面
        this.$store.commit('setWeatherIsShow',true);
    })
}

最后,看看效果:

顺便和天气预报比对一下:

可以看到,已经获取到了实时天气。

本章代码(github):
客户端vue部分
服务端Java部分

谢谢观看

祝大家春节愉快,提前拜个早年