亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

java WebService + C# winform實現軟件更新功能

系統 1810 0

??? 由于項目的需求的變動,客戶想要把原來由 javaEE 開發的 B/S 架構一個系統平臺換為 C/S 架構的,考慮到項目進度和效率的問題,項目組決定采用 C# winform 來實現客戶端的開發,而服務器端直接引用原有的系統業務。考慮到客戶端軟件可能以后會不斷地需要更新,因此做了一個軟件自動更新的功能。閑話少說,轉到正題!

首先我先要介紹一下該功能的總體實現思路:

首先考慮的是在服務端要有哪些方法來實現軟件的更新功能呢?

一、軟件需要更新,必然涉及到文件的讀取操作,因此我們要有一個讀取文件的方法;

二、軟件更新的過程中需要用進度條來展示更新的進度,因此我們服務端還需要有一個獲取文件大小的方法;

三、這是最重要的一點,就是客戶端該如何來確認是否需要更新,更新那些文件?因此我們需要用一個 xml 文件來描述這些信息。

其次要考慮一下客戶端的實現方式了,客戶端應該如何實現呢?

一、 客戶端首先要判斷軟件是否需要更新,要更新那些文件,因此我們必須先要把服務器上對軟件更新的 xml 描述文件先從服務端下載下來,然后與客戶端上的 xml 文件進行比較,看是否需要更新;

二、 若通過 xml 文件比較后,發現需要更新后,讀取 xml 文件中需要更新的文件列表,然后依次下載需要更新的文件到臨時的更新文件夾;

三、 停止主程序進程,替換掉程序中原有的文件,最后關閉更新程序,啟動主程序,更新完成!

?

實現程序更新的效果圖:

?


java WebService + C# winform實現軟件更新功能
?

?

現在我們就根據我們的總體實現思路來一步一步完成該應用的實現:

一、 WebService 的開發源碼
根據上面的思路我們分析出實現該應用我們至少需要兩個方法,一個是讀取文件的方法,一個是獲取文件大小的方法,本人采用的是 JAX-WS 2.1 來實現 WebService 的,采用其他的服務類庫也可以,只要實現該服務就可以了,我的服務實現類如下:

?

    package com.updatesoft.service;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLDecoder;

/**
 * 更新軟件操作類
 * @author jin
 *
 */
public class UpdateSoft {

	/**
	 * 獲取文件大小
	 * @param fileName 文件名稱
	 * @return 文件大小(字節)
	 */
	public long getFileSize(String fileName) {
		int nFileLength = -1;   
		try {
			
			String str = URLDecoder.decode(getClass().getClassLoader().getResource("com").toString(),"UTF-8");
			str= str.substring(0, str.indexOf("WEB-INF/classes")); 
			str=str.substring(6);
			System.out.println("路徑:" + str);
			
			File file = new File(str + fileName);
			if (file.exists()) {
				FileInputStream fis = null;
				fis = new FileInputStream(file);
			    nFileLength = fis.available();
			} else {
				System.out.println("文件不存在");
			}

		}catch (IOException e) {   
			e.printStackTrace();   
		}catch (Exception e) {   
			e.printStackTrace();   
		}   
		System.out.println(nFileLength);
		return nFileLength;
	}

	/**
	 * 根據偏移量和字節緩存大小分段獲取文件字節數組
	 * @param fileName 文件名稱
	 * @param offset 字節偏移量
	 * @param bufferSize 字節緩存大小
	 * @return 文件字節數組
	 */
	public byte[] getUpdateFile(String fileName, int offset, int bufferSize) {
		byte[] ret = null;   
		try { 
			String str = URLDecoder.decode(getClass().getClassLoader().getResource("com").toString(),"UTF-8");
			str= str.substring(0, str.indexOf("WEB-INF/classes")); 
			str=str.substring(6);
			File file = new File(str + fileName);
			
		    if (!file.exists()) {   
		        return null;   
		    }   
		    FileInputStream in = new FileInputStream(file);   
		    ByteArrayOutputStream out = new ByteArrayOutputStream(1024);   
		    byte[] b = new byte[1024];   
		    int n;
		    int t = 0;
		    while ((n = in.read(b)) != -1) { 
		    	if(t >= offset && t< offset + bufferSize){
		    		out.write(b, 0, n);
		    	}
		    	t += n;
		    }   
		    in.close();   
		    out.close();   
		    ret = out.toByteArray();
		} catch (IOException e) {   
		    e.printStackTrace();   
		}   
		return ret;
	}
}
  

?

客戶端所需要調用的服務方法我們已經實現了,接下來我們需要準備我們軟件更新的資源了(即需要更新的文件和更新文件的描述文件 update.xml )。資源文件根據需求上傳到服務器中,其中 update.xml 文件格式如下:

    <?xml version="1.0" encoding="UTF-8"?>
<update>  
    <forceUpdate>false</forceUpdate>  
    <version>20100812</version>  
    <subversion>1</subversion>  
    <filelist count="5">  
        <file name="music/陳瑞 - 白狐.mp3">true</file>
        <file name="music/韓紅 - 擦肩而過.mp3">true</file> 
		<file name="music/林俊杰 - 背對背擁抱.mp3">true</file>
        <file name="music/油菜花-成龍.mp3">true</file>
		<file name="music/鄭智化 - 別哭我最愛的人.mp3">true</file>
    </filelist>  
    <executeFile>SystemUpdateClient.exe</executeFile>  
</update>
  

?

根節點為 update forceUpdate 為是否強制更新, ture 則為是, false 則為否; version 為主版本號, subversion 為次版本號, flielist 為需要更新的文件列表,屬性 count 指定需要更新的文件數, flie 為文件節點, name 屬性指定文件名稱,值 true 為需要更新,值 false 為不需要更新。 executeFile 指定軟件更新完成后需要重新啟動的可執行文件。

?

二、 客戶端的開發源碼

客戶端的實現也比較簡單,本人采用的是 vs2008 的開發工具,在解決方案中新建一個軟件更新的窗體,在窗體中拖入一個文本框和兩個進度條,文本框用于顯示更新過程,兩個進度條一個用于顯示總進度,一個顯示單個文件進度。為了 解決多線程環境中跨線程改寫 ui 控件屬性問題,我這里采用了代理方法,實現代碼如下:

?

    using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using System.Xml;

namespace SystemUpdateClient
{
    public partial class update : Form
    {
        /// <summary>   
        /// 每次下載并寫入磁盤的文件數據大小(字節)   
        /// </summary>   
        private static int BUFFER_SIZE = 15 * 1024;   
  
        //把窗體改為單例模型   
        private static update updateForm;   
        public static update getUpdateForm()   
        {   
            if (updateForm == null)   
            {   
                updateForm = new update();   
            }   
            return updateForm;   
        }   
        //構造函數改為私有,外部程序不可以使用 new() 來創建新窗體,保證了窗體唯一性   
        private update()   
        {      
            InitializeComponent();   
        }

        //******** 定義代理方法,解決多線程環境中跨線程改寫 ui 控件屬性,開始 ********

        //定義設置一個文本的委托方法(字符串)
        private delegate void setText(string log);
        //定義設置一個進度的委托方法(整型)
        private delegate void setProcess(int count);

        //設置總進度條的最大數
        private void setProgressBar1_Maximum(int count)
        {
            progressBar1.Maximum = count;
        }
        //設置單文件進度條的最大數
        private void setProgressBar2_Maximum(int count)
        {
            progressBar2.Maximum = count;
        }
        //設置總進度條的當前值
        private void setProgressBar1_value(int count)
        {
            progressBar1.Value = count;
        }
        //設置單文件進度條當前值
        private void setProgressBar2_value(int count)
        {
            progressBar2.Value = count;
        }
        //設置總文件進度條步進進度
        private void addProgressBar1_value(int count)
        {
            if (progressBar1.Maximum > progressBar1.Value)
            {
                progressBar1.Value += count;
            }
            else
            {
                progressBar1.Value = progressBar1.Maximum;
            }
        }
        //設置單文件進度條步進進度
        private void addProgressBar2_value(int count)
        {
            if (progressBar2.Maximum > progressBar2.Value)
            {
                progressBar2.Value += count;
            }
            else
            {
                progressBar2.Value = progressBar2.Maximum;
            }
        }
        //設置文本框的值
        private void UpdateText(string log)
        {
            textBox1.Text += log;
        }

        //******** 定義代理方法,解決多線程環境中跨線程改寫 ui 控件屬性  結束 ********

        /// <summary>
        /// 窗體顯示時,調用 invokeThread 方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void update_Shown(object sender, EventArgs e)
        {
            invokeThread();
        }

        /// <summary>
        /// 開啟一個線程,執行 update_function 方法
        /// </summary>
        void invokeThread()
        {
            Thread th = new Thread(new ThreadStart(update_function));
            th.Start();
        }

        /// <summary>
        /// 自動更新方法,整合實現下面的業務邏輯。
        /// </summary>
        private void update_function()
        {
            //判斷 位于本地客戶端程序文件夾 update 是否存在
            if (Directory.Exists(Application.StartupPath + "/update"))
            {
                //存在則刪除,true 表示移除包含的子目錄及文件
                Directory.Delete("update/", true);
            }
            try{
                //通過 webservice 從服務器端獲取更新腳本文件 update.xml
                getUpdateXMLFile();
            }
            catch (Exception e)
            {
                MessageBox.Show("無法進行更新,訪問服務器失敗!\n\r原因:" + e.Message, "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }

            //判斷強制更新開關
            if (isForceUpdate())
            {
                //通過 webservice 從服務器端下載更新程序文件
                downloadFiles();
            }
            else
            {
                //比較版本號
                if (verifyVersion())
                {
                    //通過 webservice 從服務器端下載更新程序文件
                    downloadFiles();
                }
            }
            DialogResult result = MessageBox.Show("更新完成!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
            if (result == DialogResult.OK)
            {
                //啟動客戶端主程序,退出更新程序
                appExit();
            }
        }

        /// <summary>
        /// 下載 update.xml
        /// </summary>
        private void getUpdateXMLFile()
        {
            //執行委托方法,更新文本控件內容
            textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在從服務器下載 更新腳本文件 update.xml \r\n" });
            
            //創建一個文件傳送的 webservice 接口實例
            updateservice.UpdateSoftDelegateClient sendFileWS = new updateservice.UpdateSoftDelegateClient();
       
            //通過 webservice接口 獲取服務器上 update.xml 文件的長度。
            long fileSize = sendFileWS.getFileSize("update.xml");
            //判斷本地客戶端文件夾下 update 目錄是否存在
            if (!Directory.Exists(Application.StartupPath + "/update"))
            {
                //不存在則創建 update 目錄
                Directory.CreateDirectory(Application.StartupPath + "/update");
            }
            //通過定義文件緩沖區分塊下載 update.xml 文件
            for (int offset = 0; offset < fileSize; offset += BUFFER_SIZE)
            {
                //從服務器讀取指定偏移值和指定長度的二進制文件字符數組
                byte[] bytes = sendFileWS.getUpdateFile("update.xml", offset, BUFFER_SIZE);
                //如果 字符數組不為空
                if (bytes != null)
                {
                    //以追加方式打開 update.xml 文件 
                    using (FileStream fs = new FileStream(Application.StartupPath + "/update/update.xml", FileMode.Append))
                    {
                        //寫入數據
                        fs.Write(bytes, 0, bytes.Length);
                        fs.Close();
                    }
                }
            }
           

        }

        /// <summary>
        /// 是否開啟強制更新。
        /// </summary>
        /// <returns>true 開啟強制更新,false 比較版本號后再更新</returns>
        private bool isForceUpdate()
        {
            try
            {
                //開始解析 update/update.xml 新文件
                XmlDocument doc = new XmlDocument();
                doc.Load("update/update.xml");
                XmlElement root = doc.DocumentElement;
                //節點是否存在
                if (root.SelectSingleNode("forceUpdate") != null)
                {
                    //獲取 forceUpdate 節點的內容
                    string forceUpdate = root.SelectSingleNode("forceUpdate").InnerText;
                    doc = null;
                    if (forceUpdate.Equals("true"))
                    {
                        textBox1.Invoke(new setText(this.UpdateText), new object[] { "強制更新開關已打開,不再匹配版本號。 \r\n" });
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    doc = null;
                    return false;
                }


            }
            catch
            {
                //發生異常,則更新程序,覆蓋 update.xml
                MessageBox.Show("版本文件解析異常,服務器端 update.xml 可能已經損壞,請聯系管理員。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return true;
            }
        }


        /// <summary>
        ///  解析 update.xml 文件,比較version 和 subversion 判斷是否有新版本
        /// </summary>
        /// <returns>true 有新版本,false 版本相同</returns>
        private bool verifyVersion()
        {
            try
            {
                if (!File.Exists("update.xml"))
                {
                    return true;
                }
                //開始解析 update.xml 舊文件
                XmlDocument doc1 = new XmlDocument();
                doc1.Load("update.xml");
                XmlElement root1 = doc1.DocumentElement;

                //開始解析 update/update.xml 新文件
                XmlDocument doc2 = new XmlDocument();
                doc2.Load("update/update.xml");
                XmlElement root2 = doc2.DocumentElement;

                if (root1.SelectSingleNode("version") != null && root1.SelectSingleNode("subversion") != null && root2.SelectSingleNode("version") != null && root2.SelectSingleNode("subversion") != null)
                {
                    int old_version = Convert.ToInt32(root1.SelectSingleNode("version").InnerText);
                    int old_subversion = Convert.ToInt32(root1.SelectSingleNode("subversion").InnerText);
                    int new_version = Convert.ToInt32(root2.SelectSingleNode("version").InnerText);
                    int new_subversion = Convert.ToInt32(root2.SelectSingleNode("subversion").InnerText);

                    doc1 = null;
                    doc2 = null;

                    textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在判斷版本號...\r\n" });
                    //判斷版本號和子版本號
                    if (old_version == new_version && old_subversion == new_subversion)
                    {
                        textBox1.Invoke(new setText(this.UpdateText), new object[] { "已經是最新版本,無需更新\r\n" });
                        return false;
                    }
                    else
                    {
                        textBox1.Invoke(new setText(this.UpdateText), new object[] { "發現新版本,開始讀取更新列表 \r\n" });
                        return true;
                    }
                }
                else
                {
                    textBox1.Invoke(new setText(this.UpdateText), new object[] { "無法解析版本號,將下載更新全部文件...\r\n" });
                    doc1 = null;
                    doc2 = null;
                    return true;
                }
            }
            catch
            {
                //發生異常,則更新程序,覆蓋 update.xml
                MessageBox.Show("版本文件解析異常,服務器端 update.xml 可能已經損壞,請聯系管理員。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                
                return true;
            }
        }

        /// <summary>
        /// 解析 update.xml,下載更新文件
        /// </summary>
        public void downloadFiles()
        {
            //解析 update.xml
            XmlDocument doc = new XmlDocument();
            doc.Load("update/update.xml");
            XmlElement root = doc.DocumentElement;
            XmlNode fileListNode = root.SelectSingleNode("filelist");
            //獲取更新文件的數量
            int fileCount = Convert.ToInt32(fileListNode.Attributes["count"].Value);
            //調用委托方法,更新控件內容。
            textBox1.Invoke(new setText(this.UpdateText), new object[] { "需更新文件數量 " + fileCount.ToString() + "\r\n" });
            
            //結束 SystemUpdateClient.exe 進程
            System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcesses();
            foreach (System.Diagnostics.Process process in processes)
            {
                if (process.ProcessName == "SystemUpdateClient.exe")
                {
                    process.Close();
                    break;
                }
            }

            //總文件大小,用于設置總進度條最大值
            long totalFileSize = 0;

            //循環文件列表,獲取總文件的大小
            for (int i = 0; i < fileCount; i++)
            {
                XmlNode itemNode = fileListNode.ChildNodes[i];
                //獲取更新文件名
                string fileName = itemNode.Attributes["name"].Value;
                //獲取需要更新文件的總大小,調用 webservice 接口
                updateservice.UpdateSoftDelegateClient sendFileWS = new updateservice.UpdateSoftDelegateClient();
                //獲取文件長度(字節)
                long fileSize = sendFileWS.getFileSize(fileName);
                totalFileSize += fileSize;
            }

            //調用委托方法,設置總進度條的最大值。
            progressBar1.Invoke(new setProcess(this.setProgressBar1_Maximum), new object[] { (int)(totalFileSize / BUFFER_SIZE) + 1 });
            //調用委托方法,更新控件內容。
            textBox1.Invoke(new setText(this.UpdateText), new object[] { "開始更新...\r\n" });

            //循環文件列表
            for (int i = 0; i < fileCount; i++)
            {
                XmlNode itemNode = fileListNode.ChildNodes[i];
                //獲取更新文件名
                string fileName = itemNode.Attributes["name"].Value;
                //調用委托方法,更新控件內容。
                textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在下載文件 " + fileName + "\r\n" });
                //分塊下載文件,調用 webservice 接口
                updateservice.UpdateSoftDelegateClient sendFileWS = new updateservice.UpdateSoftDelegateClient();
                //獲取文件長度(字節)
                long fileSize = sendFileWS.getFileSize(fileName);
                //調用委托方法,更新進度條控件內容。
                progressBar2.Invoke(new setProcess(this.setProgressBar2_Maximum), new object[] { (int)(fileSize / BUFFER_SIZE) + 1 });
                progressBar2.Invoke(new setProcess(this.setProgressBar2_value), new object[] { 0 });
                //通過 webservice 接口 循環讀取文件數據塊,每次向前步進 BUFFER_SIZE
                for (int offset = 0; offset < fileSize; offset += BUFFER_SIZE)
                {
                    Byte[] bytes = sendFileWS.getUpdateFile(fileName, offset, BUFFER_SIZE);
                    if (bytes != null)
                    {
                        
                        if (fileName.LastIndexOf("/") != 0)
                        {
                            string newpath = fileName.Substring(0, fileName.LastIndexOf("/"));
                            if (!Directory.Exists(Application.StartupPath + "/update/" + newpath))
                            {
                                //不存在則創建 update 目錄
                                Directory.CreateDirectory(Application.StartupPath + "/update/" + newpath);
                            }
                        }
                        //將下載的更新文件寫入程序目錄的 update 文件夾下
                        using (FileStream fs = new FileStream(Application.StartupPath + "/update/" + fileName, FileMode.Append))
                        {
                            fs.Write(bytes, 0, bytes.Length);
                            fs.Close();
                        }
                    }
                    bytes = null;
                    progressBar2.Invoke(new setProcess(this.addProgressBar2_value), new object[] { 1 });
                    progressBar1.Invoke(new setProcess(this.addProgressBar1_value), new object[] { 1 });
                }
                //替換文件
                try
                {
                    if (fileName.LastIndexOf("/") != 0)
                    {
                        string newpath = fileName.Substring(0, fileName.LastIndexOf("/"));
                        if (!Directory.Exists(Application.StartupPath + "/" + newpath))
                        {
                            //不存在則創建 update 目錄
                            Directory.CreateDirectory(Application.StartupPath + "/" + newpath);
                        }
                    }
                    if (fileName != "SystemUpdateClient.XmlSerializers.dll" || fileName != "SystemUpdateClient.exe.config" || fileName != "SystemUpdateClient.pdb" || fileName != "SystemUpdateClient.exe")
                    {
                        File.Copy("update/" + fileName, fileName, true);
                    }
                }
                catch
                {
                    textBox1.Invoke(new setText(this.UpdateText), new object[] { "無法復制" + fileName + "\r\n" });
                }
                //progressBar1.Invoke(new setProcess(this.addProgressBar1_value), new object[] { 1 });
            }

            textBox1.Invoke(new setText(this.UpdateText), new object[] { "更新完成,更新程序正在做最后操作\r\n" });

            //最后復制更新信息文件
            File.Copy("update/update.xml", "update.xml", true);

        }

        /// <summary>
        /// 啟動客戶端主程序,退出更新程序
        /// </summary>
        private void appExit()
        {
            //判斷 位于本地客戶端程序文件夾 update 是否存在
            if (Directory.Exists(Application.StartupPath + "/update"))
            {
                //存在則刪除,true 表示移除包含的子目錄及文件
                Directory.Delete("update/", true);
            }

            //獲取主程序執行文件名
            XmlDocument doc = new XmlDocument();
            doc.Load("update.xml");
            XmlElement root = doc.DocumentElement;
            string executeFile = string.Empty;
            //節點是否存在
            if (root.SelectSingleNode("executeFile") != null)
            {
                //獲取 executeFile 節點的內容
                executeFile = root.SelectSingleNode("executeFile").InnerText;
            }
            doc = null;
            //啟動客戶端程序
            System.Diagnostics.Process.Start(Application.StartupPath + @"\" + executeFile);
            //更新程序退出
            Application.Exit();
        }
       
    }
}

  

?

通過源代碼大家可以通過方法 update_function() 看出該應用的流程來,它首先是從服務端下載 update.xml 文件 ( 調用 getUpdateXMLFile()) ,根據下載的 xml 文件判斷是否需要強制更新(調用 isForceUpdate() ),若是需要強制更新,那么將會強制更新所有的文件(調用 downloadFiles() ),若不需要強制更新則比較版本號(調用 verifyVersion() ),若版本號不同,則更新客戶端軟件,執行更新操作(調用 downloadFiles() ),更新完成后退出更新程序,啟動主程序的可執行文件(調用 appExit() )。

到此我們整個軟件更新應用算是已經完成了,關于代碼的具體含義,方法的執行內容,大家看一下代碼就明白了,很好理解的! ?

java WebService + C# winform實現軟件更新功能


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 九九99视频在线观看视频观看 | 成人一级黄色毛片 | 一级毛片在线 | 一级国产视频 | a拍拍男女免费看全片 | 丁香色婷婷 | 久久夜色撩人精品国产 | 成人观看网站a | 欧美三级一区二区三区 | 国产在线原创剧情麻豆 | 国产成人亚洲欧美三区综合 | 亚洲精品中文字幕乱码一区二区 | 色婷婷综合激情 | 久久精品免视国产 | 免费福利影院 | 日韩欧美一区二区三区久久 | 国产精品久久久久免费 | 奇米网奇米色 | 99这里只精品热在线获取 | 国产或人精品日本亚洲77美色 | 中文字幕日韩专区 | 久久国产影视 | 久久久久久久国产a∨ | 日本在线观看一级高清片 | 久久影院视频 | 97午夜影院 | 国产视频一区二区三区四区 | 中文字幕欧美日韩一 | 亚洲视频一区在线观看 | 国产一区欧美二区 | 亚洲精品综合欧美一区二区三区 | 九九九| 色婷婷综合在线 | 国产欧美日本在线观看 | 国产在线拍国产拍拍偷 | 欧美精品一区二区在线观看 | 久青草影院在线观看国产 | 成人短视频在线观看 | 99在线热播 | 国产福利在线观看永久免费 | 99久久国产 |