移动端与Web端疫情数据展示

2023-02-15,,,

1、题目要求

  

2、整体思想

  首先是在前两阶段已经完成的echarts可视化、利用Jsoup爬取疫情数据基础上来进行调用与完善。大致思想是在Android Studio上完成交互去调用ecplise中的Servlet,我新建了两个Servlet为PaquServlet、SearchServlet分别用来接受进行移动端的请求,与Web端的YqServlet分开来,当然也可以不新建Servlet直接调用Web端的YqServlet也是可以的。PaquServlet就是接收到移动端的调用之后开始执行,爬取疫情数据。SearchServlet当接收到移动端的请求调用后开始执行查找功能,这里我分为了两个方法,一个用来查找国内疫情数据,另一个用来查找海外的数据。而国内与海外都存放在同一个数据库表中,所以我又在数据库表中添加了一个Kind的栏位,里面的值为1或2,爬取数据的时候,国内与海外的数据分开爬取,国内的数据Kind等于1,海外的数据Kind等于2,这样查询国内或海外的数据的时候就就方便了,SearchServlet中返回的json数据在Android Studio中解析出来后,我利用的是哈希表来完成分配数据的,使用的是LinearLayout中ListView布局,由于对Android Studio的不熟悉,解析与显示数据以及布局也是在开发过程中最让我头疼的一部分了。注意:本文采用的是ecplise与Android Studio交互远程连接数据库,Android Studio上面并没有直接连取数据库

 3、代码实现

  3.1 Web端(包含前两阶段代码)

   Info.java:

package Bean;

public class Info {
private int id;
private String city;
private String yisi_num;
private String date;
private String province;
private String confirmed_num;
private String cured_num;
private String dead_num;
private String newconfirmed_num;
public String getNewconfirmed_num() {
return newconfirmed_num;
}
public void setNewconfirmed_num(String newconfirmed_num) {
this.newconfirmed_num = newconfirmed_num;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getYisi_num() {
return yisi_num;
}
public void setYisi_num(String yisi_num) {
this.yisi_num = yisi_num;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getConfirmed_num() {
return confirmed_num;
}
public void setConfirmed_num(String confirmed_num) {
this.confirmed_num = confirmed_num;
}
public String getCured_num() {
return cured_num;
}
public void setCured_num(String cured_num) {
this.cured_num = cured_num;
}
public String getDead_num() {
return dead_num;
}
public void setDead_num(String dead_num) {
this.dead_num = dead_num;
} }

   Paqu.java(如同它的名字,用来爬取数据的)

package control;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import Dao.AddService; public class Paqu { public static void main(String args[]) {
refesh();
} public static void refesh() {
// TODO Auto-generated method stub
String sheng="";
String xinzeng="";
String leiji="";
String zhiyu="";
String siwang="";
String country="";
char kind;
String url = "https://wp.m.163.com/163/page/news/virus_report/index.html?_nw_=1&_anw_=1"; int i=0; try {
//构造一个webClient 模拟Chrome 浏览器
WebClient webClient = new WebClient(BrowserVersion.CHROME);
//支持JavaScript
webClient.getOptions().setJavaScriptEnabled(true);
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setActiveXNative(false);
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setThrowExceptionOnScriptError(false);
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
webClient.getOptions().setTimeout(8000);
HtmlPage rootPage = webClient.getPage(url);
//设置一个运行JavaScript的时间
webClient.waitForBackgroundJavaScript(6000);
String html = rootPage.asXml();
Document doc = Jsoup.parse(html);
//System.out.println(doc);
        //爬取国内各省数据
Element listdiv1 = doc.select(".wrap").first();
Elements listdiv2 = listdiv1.select(".province");
for(Element s:listdiv2) {
Elements span = s.getElementsByTag("span");
Elements real_name=span.select(".item_name");
Elements real_newconfirm=span.select(".item_newconfirm");
Elements real_confirm=span.select(".item_confirm");
Elements real_dead=span.select(".item_dead");
Elements real_heal=span.select(".item_heal");
sheng=real_name.text();
xinzeng=real_newconfirm.text();
leiji=real_confirm.text();
zhiyu=real_heal.text();
siwang=real_dead.text();
System.out.println(sheng+" 新增确诊:"+xinzeng+" 累计确诊:"+leiji+" 累计治愈:"+zhiyu+" 累计死亡:"+siwang);
Date currentTime=new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String time = formatter.format(currentTime);//获取当前时间
kind='1';//1代表国内省份,2代表海外,为国内外分开查询做基础
AddService dao=new AddService();
dao.add("myinfo", sheng, xinzeng, leiji, zhiyu, siwang,time,kind);//将爬取到的数据添加至数据库,注意需将“myinfo”修改为你的表名
}
        //爬取海外数据
Element listdiv11 = doc.getElementById("world_block");
Elements listdiv22 =listdiv11.select(".chart_table_nation");
for(Element s:listdiv22) {
Elements real_name=s.select(".chart_table_name");
Elements real_newconfirm=s.select(".chart_table_today_confirm");
Elements real_confirm=s.select(".chart_table_confirm");
Elements real_dead=s.select(".chart_table_dead");
Elements real_heal=s.select(".chart_table_heal");
country=real_name.text();
xinzeng=real_newconfirm.text();
leiji=real_confirm.text();
zhiyu=real_heal.text();
siwang=real_dead.text();
System.out.println(country+" 新增确诊:"+xinzeng+" 累计确诊:"+leiji+" 累计治愈:"+zhiyu+" 累计死亡:"+siwang);
Date currentTime=new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String time = formatter.format(currentTime);//获取当前时间
kind='2';//1代表国内省份,2代表海外,为国内外分开查询做基础
AddService dao=new AddService();
dao.add("myinfo", country, xinzeng, leiji, zhiyu, siwang,time,kind);//将爬取到的数据添加至数据库,注意需将“myinfo”修改为你的表名
} } catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("爬取失败");
}
} }

   AddService.java(上面的Paqu.java在爬取中调用了该类,将数据添加到数据库中)

package Dao;

import java.sql.Connection;
import java.sql.Statement; import utils.DBUtil; public class AddService {
public void add(String table,String sheng,String xinzeng,String leiji,String zhiyu,String dead,String time,char kind) {
String sql = "insert into "+table+" (Province,Newconfirmed_num ,Confirmed_num,Cured_num,Dead_num,Time,Kind) values('" + sheng + "','" + xinzeng +"','" + leiji +"','" + zhiyu + "','" + dead+ "','" + time+ "','" + kind+ "')";
System.out.println(sql);
Connection conn = DBUtil.getConn();
Statement state = null;
int a = 0;
try {
state = conn.createStatement();
a=state.executeUpdate(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtil.close(state, conn);
}
}
}

    DeleteService.java(按需删除数据库中的数据,当重新爬取更新今日数据时调用)

package Dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException; import utils.DBUtil; public class DeleteService {
public boolean delete(String table,String value)
{
boolean c=false;
Connection conn=DBUtil.getConn();
PreparedStatement state=null;
String sql="delete from "+table+" where date(Time) =?";//date(Time)将数据库表中Time转换为只有日期的形式
try {
state=conn.prepareStatement(sql);
state.setString(1,value);
int num = state.executeUpdate();
if(num!=0)
{
c= true;
}
state.close();
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return c;
}
}

   Get.java(SearchServlet查询表中数据时调用并以List形式返回)

package Dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List; import Bean.Info;
import utils.DBUtil; public class Get {
  //查询国内各省数据
public List<Info> listAll(String date1,String date2) {
ArrayList<Info> list = new ArrayList<>();
Connection conn=DBUtil.getConn();
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql="select * from myinfo where Kind ='1' and Time between ? and ?";
try {
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, date1);
pstmt.setString(2, date2);
rs = pstmt.executeQuery();
while (rs.next()) {
Info yq = new Info();
yq.setId(rs.getInt(1));
yq.setDate(rs.getString(8));
yq.setProvince(rs.getString(2));
yq.setNewconfirmed_num(rs.getString(3));
yq.setConfirmed_num(rs.getString(4));
yq.setCured_num(rs.getString(6));
yq.setDead_num(rs.getString(7));
list.add(yq);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return list;
}
//查询海外数据
public List<Info> listAll2(String date1,String date2) {
ArrayList<Info> list = new ArrayList<>();
Connection conn=DBUtil.getConn();
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql="select * from myinfo where Kind ='2' and Time between ? and ?";
try {
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, date1);
pstmt.setString(2, date2);
System.out.println(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
Info yq = new Info();
yq.setId(rs.getInt(1));
yq.setDate(rs.getString(8));
yq.setProvince(rs.getString(2));
yq.setNewconfirmed_num(rs.getString(3));
yq.setConfirmed_num(rs.getString(4));
yq.setCured_num(rs.getString(6));
yq.setDead_num(rs.getString(7));
list.add(yq);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return list;
}
}

   Select.java(查询表中是否有今日数据从而判断是否删除.....现在发现根本不需要该方法,直接删除即可,不需要判断表中有没有数据)

package Dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List; import utils.DBUtil; public class Select {
public boolean select(String time) {
// TODO Auto-generated method stub
Connection conn=DBUtil.getConn();
PreparedStatement pstmt = null;
ResultSet rs = null;
boolean b=false;
String sql="select * from myinfo where date(Time) = ?";
System.out.println(sql);
try {
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, time);
rs = pstmt.executeQuery();
while (rs.next()) {
b=true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return b;
}
}

   PaquServlet.java(这阶段新建的,专门用来接收移动端爬取请求的)

package Servlet;

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date; import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import com.google.gson.Gson; import Dao.DeleteService;
import control.Paqu;
import utils.DBUtil; @WebServlet("/PaquServlet")//移动端爬取用到了该Servlet
public class PaquServlet extends HttpServlet { @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
} @Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("request--->"+request.getRequestURL()+"===="+request.getParameterMap().toString());
response.setContentType("text/html;charset=utf-8");
Date currentTime=new Date();
SimpleDateFormat formatter_date = new SimpleDateFormat("yyyy-MM-dd");
String date=formatter_date.format(currentTime);
DeleteService ds=new DeleteService();
ds.delete("myinfo", date);
Paqu pq=new Paqu();
pq.refesh();
} }

  SearchServlet.java(也是这阶段新建的,用来接收移动端的查找请求)

package Servlet;

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List; import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import com.google.gson.Gson; import Bean.Info;
import Dao.DeleteService;
import Dao.Get;
import Dao.Select;
import control.Paqu;
import utils.DBUtil; @WebServlet("/SearchServlet")//移动端用到了该Servlet
public class SearchServlet extends HttpServlet { @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
} @Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("request--->"+request.getRequestURL()+"===="+request.getParameterMap().toString());
response.setContentType("text/html;charset=utf-8");
String method = request.getParameter("method");
String date1 = request.getParameter("username"); // 获取客户端传过来的参数,移动端的参数叫username与password,我没有修改,可以修改为易于理解的date1,date2,但移动端也要对应修改
String date2 = request.getParameter("password");
Get get=new Get();
List<Info> list=null;
if(method.equals("province")) {//查询中国省份疫情数据
list = get.listAll(date1,date2);
}else
if(method.equals("country")) {//查询海外疫情数据
list = get.listAll2(date1, date2);
}
request.setAttribute("list",list);
Gson gson = new Gson();
String json = gson.toJson(list);
try {
response.getWriter().println(json);
// 将json数据传给客户端
} catch (Exception e) {
e.printStackTrace();
} finally {
response.getWriter().close(); // 关闭这个流,不然会发生错误的
}
}
}

   YqSearch.java(前两个阶段中Web端使用的,移动端没有调用该Servlet)

package Servlet;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List; import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import com.google.gson.Gson; import Bean.Info;
import Dao.DeleteService;
import Dao.Get;
import Dao.Select;
import control.Paqu; /**
* Servlet implementation class SearchConfirmedServlet
*/
@WebServlet("/YqServlet")
public class YqServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
Get get=new Get();
/**
* @see HttpServlet#HttpServlet()
*/
public YqServlet() {
super();
// TODO Auto-generated constructor stub
} /**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if(method.equals("getAllProvince")) {
try {
getAllProvince(request, response);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else if(method.equals("getAllConfirmed")) {
getAllConfirmed(request, response);
}
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
protected void getAllProvince(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, ParseException {
response.setCharacterEncoding("UTF-8");
Select s=new Select();
Date currentTime=new Date();
SimpleDateFormat formatter_date = new SimpleDateFormat("yyyy-MM-dd");
String date=formatter_date.format(currentTime);
String date1 = request.getParameter("date1");
String date2 = request.getParameter("date2");
Date today=formatter_date.parse(date);//将现在的date转为日期,方便比较
Date date22=formatter_date.parse(date2);//将date2转为日期,方便比较
if(today.before(date22)) {//如果今天日期today比查询后边的date2日期早,需要用到今天的数据
//不管数据库中有没有今天的数据,运行到这都需要重新爬取一遍,防止官方更新今日数据
boolean b=s.select(date);//查找数据库中是否存在今天的数据.............黄色部分可删除,前面说到了,用不到查询判断表中是否有今日数据,直接删除就好,反正下面会重新爬取存到数据库
if(b) {//如果有今日数据已存在,先删除
DeleteService ds=new DeleteService();
ds.delete("myinfo", date);
}
Paqu pq=new Paqu();//不管数据库是否存在今日数据都会爬取;如果存在,前面已经删除过了,这里的爬取就相当于更新了
pq.refesh();
}
List<Info> list = get.listAll(date1,date2);
request.setAttribute("list",list);
request.setAttribute("date1",date1);
request.setAttribute("date2",date2);
request.getRequestDispatcher("bar.jsp").forward(request, response);
} protected void getAllConfirmed(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
String date1 = request.getParameter("date1");
String date2 = request.getParameter("date2");
System.out.println(date1);
System.out.println(date2);
List<Info> list = get.listAll(date1,date2);
HttpSession session = request.getSession();
session.setAttribute("list",list);
Gson gson = new Gson();
String json = gson.toJson(list);
response.getWriter().write(json);
}
}

  DBUtil.java

package utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement; public class DBUtil { public static String db_url = "jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT";//如果发布到云服务器就将localhost改为云服务器的ip
public static String db_user = "root";
public static String db_pass = "root"; public static Connection getConn () {
Connection conn = null; try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(db_url, db_user, db_pass);
} catch (Exception e) {
e.printStackTrace();
} return conn;
} public static void close (Statement state, Connection conn) {
if (state != null) {
try {
state.close();
} catch (SQLException e) {
e.printStackTrace();
}
} if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} public static void close (ResultSet rs, Statement state, Connection conn) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
} if (state != null) {
try {
state.close();
} catch (SQLException e) {
e.printStackTrace();
}
} if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} }

  index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Insert title here</title>
<link href="${pageContext.request.contextPath }/bootstrap-3.3.7-dist/css/bootstrap.min.css" rel="stylesheet">
<script src="${pageContext.request.contextPath }/js/jquery-3.3.1.min.js"></script>
<script src="${pageContext.request.contextPath }/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script> <style type="text/css">
.skyblue{
background:skyblue;
}
.pink{
background:pink;
}
*{
margin:0px;
padding:0px;
}
a{
font-size:15px;
} </style>
</head>
<body>
<div class="container">
<form action="YqServlet?method=getAllProvince" method="post">
<div class="row" style="padding-top: 20px">
<div class="col-xs-4">
<h4>起始时间:</h4>
<input type="text" class="form-control" name="date1">
</div>
<div class="col-xs-4">
<h4>终止时间:</h4>
<input type="text" class="form-control" name="date2">
</div>
<div class="col-xs-2">
<input type="submit" class="btn btn-default" value="查询">
</div>
</div>
</form> </div>
</body>
</html>

  bar.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link href="${pageContext.request.contextPath }/bootstrap-3.3.7-dist/css/bootstrap.min.css" rel="stylesheet">
<script src="${pageContext.request.contextPath }/js/jquery.min.js"></script>
<script src="${pageContext.request.contextPath }/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath }/js/echarts.min.js"></script>
</head>
<script type="text/javascript">
var dt;
function getAllConfirmed(){ var date1 = "${date1}";
var date2 = "${date2}";
$.ajax({
url:"YqServlet?method=getAllConfirmed",
async:false,
type:"POST",
data:{"date1":date1,
"date2":date2
},
success:function(data){
dt = data;
//alert(dt);
},
error:function(){
alert("请求失败");
},
dataType:"json"
}); var myChart = echarts.init(document.getElementById('yiqingchart'));
var xd = new Array(0)//长度为33
var yd = new Array(0)//长度为33
for(var i=0;i<32;i++){
xd.push(dt[i].province);
yd.push(dt[i].confirmed_num);
}
// 指定图表的配置项和数据
var option = {
title: {
text: '全国各省的确诊人数'
},
tooltip: {
show: true,
trigger: 'axis' },
legend: {
data: ['确诊人数']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
axisLabel:{
//横坐标上的文字斜着显示 文字颜色 begin
interval:0,
rotate:45,
margin:60,
textStyle:{color:"#ec6869" }
//横坐标上的文字换行显示 文字颜色end
},
data: xd
},
yAxis: {
type: 'value'
},
series: [
{
name: '确诊人数',
type: 'bar',
stack: '总量',
data: yd,
barWidth:20,
barGap:'10%'
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
}
</script>
<body>
<button class="btn btn-default" onclick="getAllConfirmed()" style="padding-top:20px;font-size:20px">柱状图</button>
<div id="yiqingchart" style="width:900px; height: 600px;"> </div>
<table class="table table-striped" style="font-size:20px">
<tr>
<td>编号</td>
<td>时间</td>
<td>省份</td>
<td>新增人数</td>
<td>确诊人数</td>
<td>治愈人数</td>
<td>死亡人数</td>
</tr>
<c:forEach items="${list}" var="info">
<tr>
<td>${info.id}</td>
<td>${info.date}</td>
<td>${info.province}</td>
<td>${info.newconfirmed_num}</td>
<td>${info.confirmed_num}</td>
<td>${info.cured_num}</td>
<td>${info.dead_num}</td>
</tr>
</c:forEach>
</table>
</body>
</html>

  3.2 移动端

  activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.testnet.MainActivity"> <!-- <EditText-->
<!-- android:id="@+id/et_data_uname"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:hint="请输入开始时间:"-->
<!-- android:text="2020-03-18"/>--> <!-- <EditText-->
<!-- android:id="@+id/et_data_upass"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:hint="请输入截止时间:"-->
<!-- android:text="2020-03-19" />--> <TextView
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="请选择开始时间"
android:textSize="50sp" /> <DatePicker
android:id="@+id/et_data_uname"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="4dp"
android:datePickerMode="spinner"
android:calendarViewShown="false"/> <TextView
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="请选择截止时间"
android:textSize="50sp" /> <DatePicker
android:id="@+id/et_data_upass"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="4dp"
android:datePickerMode="spinner"
android:calendarViewShown="false"/> <Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loginGet"
android:text="爬取(只可获取当天数据)" /> <Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loginPOST"
android:text="查询国内疫情信息" /> <Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="loginPOST2"
android:text="查询海外疫情信息" /> </LinearLayout>

  content_main.xml(大的表单)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="50dp">
<TextView
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginLeft="10dp"
android:ellipsize="marquee"
android:gravity="center"
android:singleLine="true"
android:text="省份"
android:textSize="20sp" /> <TextView
android:id="@+id/tv_date"
android:layout_width="95dp"
android:layout_height="wrap_content"
android:text="时间"
android:textSize="20sp" /> <TextView
android:id="@+id/tv_confirmed"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="确诊"
android:textSize="15sp" /> <TextView
android:id="@+id/tv_cured"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="治愈"
android:textSize="15sp" /> <TextView
android:id="@+id/tv_dead"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="死亡"
android:textSize="15sp" /> <TextView
android:id="@+id/tv_new"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="新增"
android:textSize="15sp" />
</LinearLayout>
<ListView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/lv_main"/>
</LinearLayout>

  list_item.xml(显示具体的一条一条数据)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"> <TextView
android:id="@+id/tv_province"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginLeft="10dp"
android:ellipsize="marquee"
android:gravity="center"
android:singleLine="true"
android:text="省份"
android:textSize="20sp" /> <TextView
android:id="@+id/tv_date"
android:layout_width="95dp"
android:layout_height="wrap_content"
android:text="时间"
android:textSize="15sp" /> <TextView
android:id="@+id/tv_confirmed"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="67799"
android:textSize="20sp" /> <TextView
android:id="@+id/tv_cured"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="56002"
android:textSize="20sp" /> <TextView
android:id="@+id/tv_dead"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="3"
android:textSize="20sp" /> <TextView
android:id="@+id/tv_new"
android:layout_width="55dp"
android:layout_height="80dp"
android:text="5"
android:textSize="20sp" /> </LinearLayout>

只需用到info和两个activity即可,另外两个用不到

  Info.java

package com.example.testnet;

public class Info {
private String id;
private String city;
private String yisi_num;
private String date;
private String province;
private String confirmed_num;
private String cured_num;
private String dead_num;
private String newconfirmed_num;
public String getNewconfirmed_num() {
return newconfirmed_num;
}
public void setNewconfirmed_num(String newconfirmed_num) {
this.newconfirmed_num = newconfirmed_num;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getYisi_num() {
return yisi_num;
}
public void setYisi_num(String yisi_num) {
this.yisi_num = yisi_num;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getConfirmed_num() {
return confirmed_num;
}
public void setConfirmed_num(String confirmed_num) {
this.confirmed_num = confirmed_num;
}
public String getCured_num() {
return cured_num;
}
public void setCured_num(String cured_num) {
this.cured_num = cured_num;
}
public String getDead_num() {
return dead_num;
}
public void setDead_num(String dead_num) {
this.dead_num = dead_num;
}
}

  MainActivity.java

package com.example.testnet;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.DatePicker; import androidx.appcompat.app.AppCompatActivity; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap; public class MainActivity extends AppCompatActivity { String TAG = MainActivity.class.getCanonicalName();
// private EditText et_data_uname;
// private EditText et_data_upass;
private DatePicker et_data_uname;
private DatePicker et_data_upass;
private HashMap<String, String> stringHashMap;
private String t;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_data_uname = (DatePicker) findViewById(R.id.et_data_uname);
et_data_upass = (DatePicker) findViewById(R.id.et_data_upass);
stringHashMap = new HashMap<>();
} public void loginPOST(View view) {
stringHashMap.put("username",et_data_uname.getYear()+"-"+(et_data_uname.getMonth()+1)+"-"+et_data_uname.getDayOfMonth());
stringHashMap.put("password", et_data_upass.getYear()+"-"+(et_data_upass.getMonth()+1)+"-"+et_data_upass.getDayOfMonth()); new Thread(postRun).start();
}
public void loginPOST2(View view) {
stringHashMap.put("username",et_data_uname.getYear()+"-"+(et_data_uname.getMonth()+1)+"-"+et_data_uname.getDayOfMonth());
stringHashMap.put("password", et_data_upass.getYear()+"-"+(et_data_upass.getMonth()+1)+"-"+et_data_upass.getDayOfMonth()); new Thread(postRun2).start();
}
public void loginGet(View view) {
stringHashMap.put("username", et_data_uname.getYear()+"-"+(et_data_uname.getMonth()+1)+"-"+et_data_uname.getDayOfMonth());
stringHashMap.put("password", et_data_upass.getYear()+"-"+(et_data_upass.getMonth()+1)+"-"+et_data_upass.getDayOfMonth());
new Thread(getRun).start();
} /**
* get请求线程
*/
Runnable getRun = new Runnable() { @Override
public void run() {
// TODO Auto-generated method stub
requestGet(stringHashMap);
}
};
/**
* post请求线程
*/
Runnable postRun = new Runnable() { @Override
public void run() {
// TODO Auto-generated method stub
requestPost(stringHashMap);
}
};
Runnable postRun2 = new Runnable() { @Override
public void run() {
// TODO Auto-generated method stub
requestPost2(stringHashMap);
}
}; /**
* get提交数据
*
* @param paramsMap
*/
private void requestGet(HashMap<String, String> paramsMap) {
try {
String baseUrl = "http://10.0.2.2:8080/YiQing/PaquServlet?";//如果发布到云端,将黄色部分修改为云服务器ip
StringBuilder tempParams = new StringBuilder();
int pos = 0;
for (String key : paramsMap.keySet()) {
if (pos > 0) {
tempParams.append("&");
}
tempParams.append(String.format("%s=%s", key, URLEncoder.encode(paramsMap.get(key), "utf-8")));
pos++;
} Log.e(TAG,"params--get-->>"+tempParams.toString());
String requestUrl = baseUrl + tempParams.toString();
// 新建一个URL对象
URL url = new URL(requestUrl);
// 打开一个HttpURLConnection连接
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
// 设置连接主机超时时间
urlConn.setConnectTimeout(5 * 1000);
//设置从主机读取数据超时
urlConn.setReadTimeout(5 * 1000);
// 设置是否使用缓存 默认是true
urlConn.setUseCaches(true);
// 设置为Post请求
urlConn.setRequestMethod("GET");
//urlConn设置请求头信息
//设置请求中的媒体类型信息。
urlConn.setRequestProperty("Content-Type", "application/json");
//设置客户端与服务连接类型
urlConn.addRequestProperty("Connection", "Keep-Alive");
// 开始连接
urlConn.connect();
// 判断请求是否成功
if (urlConn.getResponseCode() == 200) {
// 获取返回的数据
String result = streamToString(urlConn.getInputStream());
Log.e(TAG, "Get方式请求成功,result--->" + result);
} else {
Log.e(TAG, "Get方式请求失败");
}
// 关闭连接
urlConn.disconnect();
} catch (Exception e) {
Log.e(TAG, e.toString());
}
} /**
* post提交数据
*
* @param paramsMap
*/
private void requestPost(HashMap<String, String> paramsMap) {
try {
String baseUrl = "http://10.0.2.2:8080/YiQing/SearchServlet?method=province";//如若发布到云端,将黄色部分修改为云端ip
//合成参数
StringBuilder tempParams = new StringBuilder();
int pos = 0;
for (String key : paramsMap.keySet()) {
if (pos >0) {
tempParams.append("&");
}
tempParams.append(String.format("%s=%s", key, URLEncoder.encode(paramsMap.get(key), "utf-8")));
pos++;
}
String params = tempParams.toString();
Log.e(TAG,"params--post-->>"+params);
// 请求的参数转换为byte数组
// byte[] postData = params.getBytes();
// 新建一个URL对象
URL url = new URL(baseUrl);
// 打开一个HttpURLConnection连接
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
// 设置连接超时时间
urlConn.setConnectTimeout(5 * 1000);
//设置从主机读取数据超时
urlConn.setReadTimeout(5 * 1000);
// Post请求必须设置允许输出 默认false
urlConn.setDoOutput(true);
//设置请求允许输入 默认是true
urlConn.setDoInput(true);
// Post请求不能使用缓存
urlConn.setUseCaches(false);
// 设置为Post请求
urlConn.setRequestMethod("POST");
//设置本次连接是否自动处理重定向
urlConn.setInstanceFollowRedirects(true);
//配置请求Content-Type
// urlConn.setRequestProperty("Content-Type", "application/json");//post请求不能设置这个
// 开始连接
urlConn.connect(); // 发送请求参数
PrintWriter dos = new PrintWriter(urlConn.getOutputStream());
dos.write(params);
dos.flush();
dos.close();
// 判断请求是否成功
if (urlConn.getResponseCode() == 200) {
// 获取返回的数据
String result = streamToString(urlConn.getInputStream());
Log.e(TAG, "Post方式请求成功,result--->" + result);
t=result;
Intent intent = new Intent(MainActivity.this,MainActivity2.class);
System.out.println(t);
intent.putExtra("data",t);
startActivity(intent);
} else {
Log.e(TAG, "Post方式请求失败");
}
// 关闭连接
urlConn.disconnect();
} catch (Exception e) {
Log.e(TAG, e.toString());
}
} private void requestPost2(HashMap<String, String> paramsMap) {
try {
String baseUrl = "http://10.0.2.2:8080/YiQing/SearchServlet?method=country";//如若发布到云端,将黄色部分修改为云端ip
//合成参数
StringBuilder tempParams = new StringBuilder();
int pos = 0;
for (String key : paramsMap.keySet()) {
if (pos >0) {
tempParams.append("&");
}
tempParams.append(String.format("%s=%s", key, URLEncoder.encode(paramsMap.get(key), "utf-8")));
pos++;
}
String params = tempParams.toString();
Log.e(TAG,"params--post-->>"+params);
// 请求的参数转换为byte数组
// byte[] postData = params.getBytes();
// 新建一个URL对象
URL url = new URL(baseUrl);
// 打开一个HttpURLConnection连接
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
// 设置连接超时时间
urlConn.setConnectTimeout(5 * 1000);
//设置从主机读取数据超时
urlConn.setReadTimeout(5 * 1000);
// Post请求必须设置允许输出 默认false
urlConn.setDoOutput(true);
//设置请求允许输入 默认是true
urlConn.setDoInput(true);
// Post请求不能使用缓存
urlConn.setUseCaches(false);
// 设置为Post请求
urlConn.setRequestMethod("POST");
//设置本次连接是否自动处理重定向
urlConn.setInstanceFollowRedirects(true);
//配置请求Content-Type
// urlConn.setRequestProperty("Content-Type", "application/json");//post请求不能设置这个
// 开始连接
urlConn.connect(); // 发送请求参数
PrintWriter dos = new PrintWriter(urlConn.getOutputStream());
dos.write(params);
dos.flush();
dos.close();
// 判断请求是否成功
if (urlConn.getResponseCode() == 200) {
// 获取返回的数据
String result = streamToString(urlConn.getInputStream());
Log.e(TAG, "Post方式请求成功,result--->" + result);
t=result;
Intent intent = new Intent(MainActivity.this,MainActivity2.class);
System.out.println(t);
intent.putExtra("data",t);
startActivity(intent);
} else {
Log.e(TAG, "Post方式请求失败");
}
// 关闭连接
urlConn.disconnect();
} catch (Exception e) {
Log.e(TAG, e.toString());
}
} /**
* 将输入流转换成字符串
*
* @param is 从网络获取的输入流
* @return
*/
public String streamToString(InputStream is) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.close();
is.close();
byte[] byteArray = baos.toByteArray();
return new String(byteArray);
} catch (Exception e) {
Log.e(TAG, e.toString());
return null;
}
} /**
* 文件下载
*
* @param fileUrl
*/
private void downloadFile(String fileUrl) {
try {
// 新建一个URL对象
URL url = new URL(fileUrl);
// 打开一个HttpURLConnection连接
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
// 设置连接主机超时时间
urlConn.setConnectTimeout(5 * 1000);
//设置从主机读取数据超时
urlConn.setReadTimeout(5 * 1000);
// 设置是否使用缓存 默认是true
urlConn.setUseCaches(true);
// 设置为Post请求
urlConn.setRequestMethod("GET");
//urlConn设置请求头信息
//设置请求中的媒体类型信息。
urlConn.setRequestProperty("Content-Type", "application/json");
//设置客户端与服务连接类型
urlConn.addRequestProperty("Connection", "Keep-Alive");
// 开始连接
urlConn.connect();
// 判断请求是否成功
if (urlConn.getResponseCode() == 200) {
String filePath = "";//下载文件保存在本地的地址
File descFile = new File(filePath);
FileOutputStream fos = new FileOutputStream(descFile);
;
byte[] buffer = new byte[1024];
int len;
InputStream inputStream = urlConn.getInputStream();
while ((len = inputStream.read(buffer)) != -1) {
// 写到本地
fos.write(buffer, 0, len);
}
} else {
Log.e(TAG, "文件下载失败");
}
// 关闭连接
urlConn.disconnect();
} catch (Exception e) {
Log.e(TAG, e.toString());
}
} /**
* 文件上传
*
* @param filePath
* @param paramsMap
*/
private void upLoadFile(String filePath, HashMap<String, String> paramsMap) {
try {
String baseUrl = "http://xxx.com/uploadFile";
File file = new File(filePath);
//新建url对象
URL url = new URL(baseUrl);
//通过HttpURLConnection对象,向网络地址发送请求
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
//设置该连接允许读取
urlConn.setDoOutput(true);
//设置该连接允许写入
urlConn.setDoInput(true);
//设置不能适用缓存
urlConn.setUseCaches(false);
//设置连接超时时间
urlConn.setConnectTimeout(5 * 1000); //设置连接超时时间
//设置读取超时时间
urlConn.setReadTimeout(5 * 1000); //读取超时
//设置连接方法post
urlConn.setRequestMethod("POST");
//设置维持长连接
urlConn.setRequestProperty("connection", "Keep-Alive");
//设置文件字符集
urlConn.setRequestProperty("Accept-Charset", "UTF-8");
//设置文件类型
urlConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + "*****");
String name = file.getName();
DataOutputStream requestStream = new DataOutputStream(urlConn.getOutputStream());
requestStream.writeBytes("--" + "*****" + "\r\n");
//发送文件参数信息
StringBuilder tempParams = new StringBuilder();
tempParams.append("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + name + "\"; ");
int pos = 0;
int size = paramsMap.size();
for (String key : paramsMap.keySet()) {
tempParams.append(String.format("%s=\"%s\"", key, paramsMap.get(key), "utf-8"));
if (pos < size - 1) {
tempParams.append("; ");
}
pos++;
}
tempParams.append("\r\n");
tempParams.append("Content-Type: application/octet-stream\r\n");
tempParams.append("\r\n");
String params = tempParams.toString();
requestStream.writeBytes(params);
//发送文件数据
FileInputStream fileInput = new FileInputStream(file);
int bytesRead;
byte[] buffer = new byte[1024];
DataInputStream in = new DataInputStream(new FileInputStream(file));
while ((bytesRead = in.read(buffer)) != -1) {
requestStream.write(buffer, 0, bytesRead);
}
requestStream.writeBytes("\r\n");
requestStream.flush();
requestStream.writeBytes("--" + "*****" + "--" + "\r\n");
requestStream.flush();
fileInput.close();
int statusCode = urlConn.getResponseCode();
if (statusCode == 200) {
// 获取返回的数据
String result = streamToString(urlConn.getInputStream());
Log.e(TAG, "上传成功,result--->" + result);
} else {
Log.e(TAG, "上传失败");
}
} catch (IOException e) {
Log.e(TAG, e.toString());
}
} }

  MainActivity2.java(当点击查询后,会从MainActivity跳转到该MainActivity2)  

package com.example.testnet;

import android.content.Intent;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter; import androidx.appcompat.app.AppCompatActivity; import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; public class MainActivity2 extends AppCompatActivity { private List<Info> list ;
private YqAdapter mAdapter;
Info yq=new Info();
int n=0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.content_main);
Intent m =getIntent();
String result=m.getStringExtra("data");
List<Map<String, Object>> listitem = new ArrayList<Map<String, Object>>();
String[] province=new String[1000];
String[] date=new String[1000];
String[] now=new String[1000];
String[] cured=new String[1000];
String[] dead=new String[1000];
String[] today=new String[1000];
try {
JSONArray json = new JSONArray(result);
for (int i = 0;i<json.length();i++){
JSONObject jb=json.getJSONObject(i);
date[i]=jb.getString("date");
province[i]=jb.getString("province");
now[i]=jb.getString("confirmed_num");
cured[i]=jb.getString("cured_num");
dead[i]=jb.getString("dead_num");
today[i]=jb.getString("newconfirmed_num");
n=i+1;
}
} catch (JSONException e) {
e.printStackTrace();
}
for (int i = 0; i <n; i++)
{
Map<String,Object> map = new HashMap<String, Object>();
map.put("province",province[i]);
map.put("date",date[i]);
map.put("now",now[i]);
map.put("cured",cured[i]);
map.put("dead",dead[i]);
map.put("today",today[i]);
listitem.add(map);
}
// for (int i=0;i<n;i++){
// System.out.println(province[i]);
// }
SimpleAdapter adapter = new SimpleAdapter(this
, listitem
, R.layout.list_item
, new String[]{"province","date","now","cured","dead","today"}
,new int[]{R.id.tv_province,R.id.tv_date,R.id.tv_confirmed,R.id.tv_cured,R.id.tv_dead,R.id.tv_new});
ListView listView =(ListView) findViewById(R.id.lv_main);
listView.setAdapter(adapter);
}
}

4、移动端与Web端的交互(上面的代码已经实现了交互)

  移动端只是向Web端发送请求,调用相应的Servlet,一些复杂的运算还是ecplise中进行完成的。

  刚开始可以参考该篇博客了解交互过程https://blog.csdn.net/qq_34317125/article/details/80533685

5、云服务器配置与部署

  可以参考我的另一篇博客https://www.cnblogs.com/xhj1074376195/p/12318178.html

  新获得的云服务要像一台新电脑一样,同样需要下载配置jdk,mysql,Tomcat....

6、运行测试结果

  Web端

  移动端

7、开发过程中出现的问题 

  1.在Android Studio中将localhost:8080修改为云端服务器ip时,没有把:8080去掉,导致连接不上云端服务器。

  2.Listview的布局中,没有弄清楚绑定的是哪一个页面,导致一直报空指针,应该修改为ListView控件所在的xml页面。

  3.在AndroidMainifresh.xml中添加联网设置

<uses-permission android:name="android.permission.INTERNET" />

  4.新建一个MainActivity2.java之后,在AndroidMainifest.xml中添加Activity活动

<activity android:name=".MainActivity2"/>

  5.在虚拟机上可以正常运行,但打包apk至真机无法运行,请教同学以及百度后得知,Android 9.0之后 的应用程序,将要求默认使用加密连接,这意味着 Android P(9.0) 将禁止 App 使用所有未加密的连接,因此运行 Android P(9.0) 系统的安卓设备无论是接收或者发送流量,未来都不能明码传输,需要使用下一代(Transport Layer Security)传输层安全协议,而 Android Nougat 和 Oreo 则不受影响。简单来说就是,系统为了安全起见,Android 9.0之后禁止使用不加密的连接。解决办法,在AndroidMainifresh.xml中<application></application>下添加语句允许不加密连接

        android:usesCleartextTraffic="true"

8、PSP0级时间记录日志

 9、资源分享

  项目所需要的jar包   链接:https://pan.baidu.com/s/1z4eqP3Gpa0EqudogM-a-lw    提取码:tr06

     疫情数据显示Web端源代码已上传至GitHub      https://github.com/xhj1074376195/YiQing_Web

     疫情数据显示移动端源代码已上传至GitHub  https://github.com/xhj1074376195/YiQing_App

移动端与Web端疫情数据展示的相关教程结束。

《移动端与Web端疫情数据展示.doc》

下载本文的Word格式文档,以方便收藏与打印。