您的位置:首页 > 运维架构 > 网站架构

基于微软Synchronization Services双向同步技术在企业项目中的架构应用研究 推荐

2008-06-17 14:29 555 查看
项目应用场景:

某客户是一个大型集团企业的信息部门,掌管着企业几百台服务器,并且以后会不断扩充;
为了更好的维护信息办的服务器;信息部门需要开发一套维护系统,来记录各个服务器的相关状态信息(如,IP、所安装在服务器的应用系统和相关信息等),便于维护和查询;客户维护人员可以携带笔记本脱机即时编写维修服务器相关记录信息(增、删、查、改),客户端联网即可自动同步到服务器最新维护数据。
客户要求实现服务器端和客户端双向同步的方式,客户端操作数据服务器端自动更新,反之操作服务器端,
客户端自动更新。

[align=left]早先的解决方案: [/align]
[align=left]客户端和服务端都使用SQL2005数据库,并采用数据库订阅的方式进行数据同步。
此种设计缺点是:

1、客户端必须安装SQL2005(包括SQL Server Express),造成操作客户端系统运行速度降低,占用客户端计算机系统资源大等问题。
2、同步方式配置复杂。需要订阅方式发布服务器和订阅服务器互相注册,服务器端与客户端配置复杂。 [/align]
[align=left]最新的解决方案: [/align]
[align=left] 1、客户端数据库采用微软Microsoft SQL Server Compact 3.5压缩数据库,实施技术Microsoft Synchronization Services v1.0。
2、服务器端数据库采用微软Microsoft SQL Server 2005(2008)。
3、开发工具是Microsoft Visual Studio 2008正式版。 [/align]
[align=left] 服务器端开发B/S维护系统;C/S端开发客户端Winform(WPF)应用程序;实现客户端winform系统业务操作会相应的同步到服务器中;反之,服务器端的业务系统操作也会自动同步到下属不同的客户端Compact数据库中。[/align]
[align=left]
[/align]
[align=left] 架构设计:
[/align]
[align=left] 1、服务器端系统架构设计采用MVPC架构:Web Client Software Factory [/align]
[align=left] 2、客户端业务系统架构设计也MVPC架构:Smart Client Software Factory [/align]
[align=left] 优点:客户端模块代码很容易移植到服务器端asp.net开发的业务系统;解决客户端与服务器端应用模块重复,减少代码量;

如果采用Microsoft SQL Server Compact 3.5客户端数据库(Sdf扩展名的数据库文件),不必安装Microsoft SQL Server 2005数据库,客户只需要一个安装包,即可实现客户端的应用系统安装,更加方便灵活的用户体验。 [/align]



[align=left] Sync Services for ADO.NET provider 技术应用场景

* C\S结构的离线应用,在本地缓存中心数据库中的部分数据(极特别的情况下会缓存全部)。应用程序使用缓存的数据,并在特定的时间把一批更改上次到中心数据库。[/align]
[align=left]
* 协作应用,应用程序只使用本地数据,并周期性的与其他参与者进行 Peer-to-Peer 的同步。数据库双向同步:Feature实现客户端C/S系统数据库数据与服务器端B/S管理系统数据库数据进行数据双向同步;

Use case:用户在客户端针对本地数据库系统进行增加、删除、修改等操作,如果在线联网状态,数据自动同步到远程SQL Server数据库;有管理权限用户,登录B/S系统进行增加、删除、修改等操作,SQL Server数据自动同步到客户端数据库。

功能实现Server与Client端进行数据操作(增加、删除),都能很好的进行数据双向同步;

Sync Demo源码程序截图如下: [/align]


Compact数据库同步技术功能

Microsoft SQL Server Compact 3.5 (以前称为 Microsoft SQL Server 2005 Mobile Edition)是一种压缩数据库,很适合嵌入到移动应用程序和桌面应用程序中。Microsoft SQL Server Compact 3.5 为开发本机和托管应用程序的开发人员提供了与其他 SQL Server 版本通用的编程模型。SQL Server Compact Edition 以很少的空间占用提供关系数据库功能:健壮数据存储、优化查询处理器以及可靠、可缩放的连接。

Microsoft Synchronization Services for ADO.NET 是一组 DLL,提供了一个可组构的 API。根据应用程序的体系结构和要求,可以使用提供的所有或部分组件。

Synchronization Services 实现了 SQL Server Compact 3.5 客户端数据库和服务器数据库或任何其他数据源(如以 XML 形式提供股票报价的服务)之间的同步。在同步两个数据库方面,Synchronization Services 支持使用为之提供了 ADO.NET 提供程序的任何服务器数据库的双层和 N 层体系结构。

在对客户端数据库和其他类型的数据源进行同步方面,Synchronization Services 支持基于服务的体系结构。与双层或 N 层体系结构相比,此体系结构需要编写更多的应用程序代码;但是,它不需要开发人员采取另一种不同的方式进行同步。

通过 Microsoft Visual Studio 2008 的Microsoft Synchronization Services for ADO.NET,可以通过双层、N 层和基于服务的体系结构同步来自不同来源的数据。

Synchronization Services API 提供了一组用于在数据服务和本地存储区之间同步数据的组件,而不是仅仅用于复制数据库及其架构。应用程序正越来越多地用于移动客户端,如便携式计算机和设备。由于这些移动客户端与中央服务器没有连贯或可靠的网络连接,因此对于这些应用程序而言,能够在客户端上使用数据的一份本地副本十分重要。同等重要的一点是:在网络连接可用时,需要能够将数据的本地副本与中央服务器同步。Synchronization Services API 以 ADO.NET 数据访问 API 为蓝本,提供了一种直观的数据同步手段。Synchronization Services 对构建依靠连续网络连接的应用程序这一工作进行了合乎逻辑的扩展,使我们得以针对断续连接的网络环境开发应用程序。

相关代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;

using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.SqlServerCe;
using Microsoft.Synchronization.Data.Server;
using System.Data.SqlServerCe;

namespace SynchrnoizationDemo
{
public partial class frmMain : Form
{
#region 定义常规数据区
private const string strServerListTableName = "ServerList";
private const string strUserListTableName = "UserList";

private const string strCreationTrackingColumn = @"create_timestamp";
private const string strUpdateTrackingColumn = @"update_timestamp";
private const string strDeletionTrackingColumn = @"update_timestamp";
private const string strUpdateOriginatorIdColumn = @"update_originator_id";

private const string strNoClientDBWarnString = "客户端数据库不存在,请首先执行 双向同步 命令";
#endregion

#region 定义变量区
private string strServerIP = @"(local)";
private string strServerDataBaseName = "SyncDemo";
private string strLoginUser = "sa";
private string strLoginPassWord = "sa";
private string strServerConnectString = "";

private string strClientDataBasename = Application.StartupPath + @"\ClientDB.sdf";
private string strClientConnectString = "";

private bool blnOperationClient = true;
private string strOperationTable = strServerListTableName;
#endregion

public frmMain()
{
InitializeComponent();
}

private void frmMain_Load(object sender, EventArgs e)
{
// 初始化相关参数
SetEnvironmentValue();
}

#region 通用方法
/// <summary>
/// 获取相关参数
/// </summary>
private void GetEnvironmentValue()
{
strServerIP = txtServerIP.Text.Trim();
strServerDataBaseName = txtServerDataBaseName.Text.Trim();
strLoginUser = txtLoginUser.Text.Trim();
strLoginPassWord = txtLoginPassWord.Text.Trim();
strServerConnectString = "Data Source="+strServerIP+";Initial Catalog="+strServerDataBaseName+";User ID="+strLoginUser+";Password="+strLoginPassWord+";";

strClientDataBasename = txtClientDataBaseFileName.Text.Trim();
strClientConnectString = "Data Source=" + strClientDataBasename;

blnOperationClient = rbClient.Checked;
strOperationTable = tabMain.SelectedIndex == 0 ? strServerListTableName : strUserListTableName;
}
/// <summary>
/// 显示相关参数
/// </summary>
private void SetEnvironmentValue()
{
txtServerIP.Text = strServerIP;
txtServerDataBaseName.Text = strServerDataBaseName;
txtLoginUser.Text = strLoginUser;
txtLoginPassWord.Text = strLoginPassWord;
txtClientDataBaseFileName.Text = strClientDataBasename;

if (blnOperationClient)
{
rbClient.Checked=true;
rbServer.Checked=false;
}
else
{
rbServer.Checked=true;
rbClient.Checked=false;
}
}
/// <summary>
/// 检查客户端数据库是否存在
/// </summary>
/// <returns></returns>
private Boolean CheckClientDb()
{
//这里默认已经调用了GetEnvironmentValue函数
return File.Exists(strClientDataBasename);
}
#endregion

#region 刷新数据
/// <summary>
/// 刷新调用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btRefersh_Click(object sender, EventArgs e)
{
btRefersh.Enabled = false;
//RefreshData(tabMain.SelectedTab.Text.Replace("表", ""));
RefreshData(strServerListTableName);
RefreshData(strUserListTableName);
btRefersh.Enabled = true;
}

/// <summary>
/// 数据刷新
/// </summary>
/// <param name="sTableName"></param>
private void RefreshData(string sTableName)
{
GetEnvironmentValue();

string sSql = "Select * from " + sTableName;

DataTable dtQuery;

try
{
#region 读取服务器端
SqlDataAdapter serverLoadAdapter = new SqlDataAdapter(sSql, strServerConnectString);
dtQuery = new DataTable();
serverLoadAdapter.Fill(dtQuery);
RemoveServerTrackingColumns(dtQuery);
switch (sTableName)
{
case strServerListTableName:
dgSLServer.DataSource = dtQuery;
break;
case strUserListTableName:
dgULServer.DataSource = dtQuery;
break;
}
#endregion

#region 读取客户端
if (CheckClientDb())
{
SqlCeDataAdapter clientLoadAdapter = new SqlCeDataAdapter(sSql,strClientConnectString);
dtQuery = new DataTable();
clientLoadAdapter.Fill(dtQuery);
RemoveClientTrackingColumns(dtQuery);
switch (sTableName)
{
case strServerListTableName:
dgSLClient.DataSource = dtQuery;
break;
case strUserListTableName:
dgULClient.DataSource = dtQuery;
break;
}
}
else
{
MessageBox.Show(strNoClientDBWarnString);
}
#endregion
}
catch (System.Exception e)
{
MessageBox.Show(e.Message);
}
}

/// <summary>
/// 去除客户端表中不需要显示的列
/// </summary>
/// <param name="dataTable"></param>
private static void RemoveClientTrackingColumns(DataTable dataTable)
{
if (dataTable.Columns.Contains("__sysInsertTxBsn"))
{
dataTable.Columns.Remove("__sysInsertTxBsn");
}

if (dataTable.Columns.Contains("__sysChangeTxBsn"))
{
dataTable.Columns.Remove("__sysChangeTxBsn");
}
}
/// <summary>
/// 去除服务器端表中不需要显示的列
/// </summary>
/// <param name="dataTable"></param>
private static void RemoveServerTrackingColumns(DataTable dataTable)
{
if (dataTable.Columns.Contains("update_timestamp"))
{
dataTable.Columns.Remove("update_timestamp");
}

if (dataTable.Columns.Contains("create_timestamp"))
{
dataTable.Columns.Remove("create_timestamp");
}

if (dataTable.Columns.Contains("update_originator_id"))
{
dataTable.Columns.Remove("update_originator_id");
}

}
#endregion

#region 同步数据
/// <summary>
/// 同步数据调用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btSync_Click(object sender, EventArgs e)
{
btSync.Enabled = false;
panOperation.Enabled = false;
SynchronizeData();
btRefersh_Click(null, null);
panOperation.Enabled = true;
btSync.Enabled = true;
}
/// <summary>
/// 数据同步
/// </summary>
private void SynchronizeData()
{
#region 初始化
GetEnvironmentValue();

SyncAgent syncAgent = new SyncAgent();
#endregion
try
{

#region 服务器端准备
DbServerSyncProvider serverSyncProvider = new DbServerSyncProvider();
SqlConnection serverConnection = new SqlConnection(strServerConnectString);
serverSyncProvider.Connection = serverConnection;
syncAgent.RemoteProvider = serverSyncProvider;
#endregion

#region 客户端准备
if (!CheckClientDb())
{
SqlCeEngine clientEngine = new SqlCeEngine(strClientConnectString);
clientEngine.CreateDatabase();
clientEngine.Dispose();
}
SqlCeClientSyncProvider clientSyncProvider = new SqlCeClientSyncProvider(strClientConnectString);
syncAgent.LocalProvider = clientSyncProvider;
#endregion

#region SyncTable和SyncGroup准备
// ServerList表
SyncTable tableServerList = new SyncTable(strServerListTableName);
tableServerList.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;
tableServerList.SyncDirection = SyncDirection.Bidirectional;

// UserList表
SyncTable tableUserList = new SyncTable(strUserListTableName);
tableUserList.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;
tableUserList.SyncDirection = SyncDirection.Bidirectional;

// SyncGroup
SyncGroup syncGroup = new SyncGroup("SyncDemo");
tableServerList.SyncGroup = syncGroup;
tableUserList.SyncGroup = syncGroup;

syncAgent.Configuration.SyncTables.Add(tableServerList);
syncAgent.Configuration.SyncTables.Add(tableUserList);
#endregion

#region SyncAdapter准备

#region ServerList准备
SqlSyncAdapterBuilder ServerListBuilder = new SqlSyncAdapterBuilder();
ServerListBuilder.Connection = serverConnection;
ServerListBuilder.SyncDirection = SyncDirection.Bidirectional;

// 主表及其相关列
ServerListBuilder.TableName = strServerListTableName;
ServerListBuilder.DataColumns.Add("ServerID");
ServerListBuilder.DataColumns.Add("ServerIP");
ServerListBuilder.DataColumns.Add("ServerBuyTime");

// tombstone表及其相关列
ServerListBuilder.TombstoneTableName = strServerListTableName + "_tombstone";
ServerListBuilder.TombstoneDataColumns.Add("ServerID");
ServerListBuilder.TombstoneDataColumns.Add("ServerIP");
ServerListBuilder.TombstoneDataColumns.Add("ServerBuyTime");

// 相关的跟踪列
ServerListBuilder.CreationTrackingColumn = strCreationTrackingColumn;
ServerListBuilder.UpdateTrackingColumn = strUpdateTrackingColumn;
ServerListBuilder.DeletionTrackingColumn = strDeletionTrackingColumn;
ServerListBuilder.UpdateOriginatorIdColumn = strUpdateOriginatorIdColumn;

SyncAdapter ServerListSyncAdapter = ServerListBuilder.ToSyncAdapter();
((SqlParameter)ServerListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_last_received_anchor"]).DbType = DbType.Binary;
((SqlParameter)ServerListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_new_received_anchor"]).DbType = DbType.Binary;
serverSyncProvider.SyncAdapters.Add(ServerListSyncAdapter);
#endregion

#region UserList准备
SqlSyncAdapterBuilder UserListBuilder = new SqlSyncAdapterBuilder();
UserListBuilder.SyncDirection = SyncDirection.Bidirectional;
UserListBuilder.Connection = serverConnection;

// 主表及其相关列
UserListBuilder.TableName = strUserListTableName;
UserListBuilder.DataColumns.Add("UserID");
UserListBuilder.DataColumns.Add("UserName");
UserListBuilder.DataColumns.Add("UserPW");

// tombstone表及其相关列
UserListBuilder.TombstoneTableName = strUserListTableName + "_tombstone";
UserListBuilder.TombstoneDataColumns.Add("UserID");
UserListBuilder.TombstoneDataColumns.Add("UserName");
UserListBuilder.TombstoneDataColumns.Add("UserPW");

// 相关的跟踪列
UserListBuilder.CreationTrackingColumn = strCreationTrackingColumn;
UserListBuilder.UpdateTrackingColumn = strUpdateTrackingColumn;
UserListBuilder.DeletionTrackingColumn = strDeletionTrackingColumn;
UserListBuilder.UpdateOriginatorIdColumn = strUpdateOriginatorIdColumn;

SyncAdapter UserListSyncAdapter = UserListBuilder.ToSyncAdapter();
((SqlParameter)UserListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_last_received_anchor"]).DbType = DbType.Binary;
((SqlParameter)UserListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_new_received_anchor"]).DbType = DbType.Binary;
serverSyncProvider.SyncAdapters.Add(UserListSyncAdapter);
#endregion

#endregion

#region 数据同步
SqlCommand anchorCmd = new SqlCommand();
anchorCmd.CommandType = CommandType.Text;
anchorCmd.CommandText = "Select @" + SyncSession.SyncNewReceivedAnchor + "= @@DBTS";// " = @@DBTS";
anchorCmd.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp).Direction = ParameterDirection.Output;
serverSyncProvider.SelectNewAnchorCommand = anchorCmd;

SqlCommand clientIdCmd = new SqlCommand();
clientIdCmd.CommandType = CommandType.Text;
clientIdCmd.CommandText = "SELECT @" + SyncSession.SyncOriginatorId + " = 1";
clientIdCmd.Parameters.Add("@" + SyncSession.SyncOriginatorId, SqlDbType.Int).Direction = ParameterDirection.Output;

serverSyncProvider.SelectClientIdCommand = clientIdCmd;

syncAgent.Synchronize();
#endregion

}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
}
#endregion

#region 增量操作
#region 产生各种随机数据
/// <summary>
/// 产生随机IP
/// </summary>
/// <returns></returns>
private string ReturnRandomIP(Random rand)
{
string sRe = "192.168." + rand.Next(0, 256).ToString() + "." + rand.Next(0, 256).ToString();
return sRe;
}
/// <summary>
/// 产生随机ID
/// </summary>
/// <param name="rand"></param>
/// <returns></returns>
private string ReturnRandomID(Random rand)
{
return rand.Next((int)(DateTime.Now.ToFileTime() % 10000)).ToString();
}

/// <summary>
/// 产生随机字符
/// </summary>
/// <param name="rand"></param>
/// <returns></returns>
private char ReturnRandomChar(Random rand)
{
int ret = rand.Next(122);
while (ret < 48 || (ret > 57 && ret < 65) || (ret > 90 && ret < 97))
{
ret = rand.Next(122);
}
return (char)ret;
}
/// <summary>
/// 产生随机字符串
/// </summary>
/// <param name="rand"></param>
/// <returns></returns>
private string ReturnRandomString(Random rand)
{
int length = 10;
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++)
{
sb.Append(ReturnRandomChar(rand));
}
return sb.ToString();
}
#endregion

/// <summary>
/// 返回一个随机操作的ID
/// </summary>
private string ReturnRandomUpdateOrDeleteID(Random rand)
{
DataTable dtRef = new DataTable();
if (strOperationTable == strServerListTableName)
{
if (blnOperationClient)
{
dtRef = (DataTable)dgSLClient.DataSource;
}
else
{
dtRef = (DataTable)dgSLServer.DataSource;
}
}
else
{
if (blnOperationClient)
{
dtRef = (DataTable)dgULClient.DataSource;
}
else
{
dtRef = (DataTable)dgULServer.DataSource;
}
}
int iTotalRows = dtRef.Rows.Count;
return dtRef.Rows[rand.Next(0, iTotalRows)][0].ToString();
}
/// <summary>
/// 具体执行增删改的操作
/// </summary>
/// <param name="CommandString"></param>
private void ExecuteOperation(string CommandString)
{
if (blnOperationClient)
{
#region 客户端操作
if (CheckClientDb())
{
SqlCeConnection conn = new SqlCeConnection(strClientConnectString);
SqlCeCommand cmd = new SqlCeCommand();
cmd.Connection = conn;
cmd.CommandText = CommandString.ToString();
try
{
conn.Open();
cmd.ExecuteNonQuery();
}
catch (System.Exception exp)
{
MessageBox.Show(exp.Message);
}
finally
{
conn.Close();
}
}
else
{
MessageBox.Show(strNoClientDBWarnString);
}
#endregion
}
else
{
#region 服务器端操作
SqlConnection conn = new SqlConnection(strServerConnectString);
SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = CommandString;
try
{
conn.Open();
cmd.ExecuteNonQuery();
}
catch (System.Exception exp)
{
MessageBox.Show(exp.Message);
}
finally
{
conn.Close();
}
#endregion
}

RefreshData(strOperationTable);
}

/// <summary>
/// 更新
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btUpdate_Click(object sender, EventArgs e)
{
btUpdate.Enabled = false;

#region 初始化
GetEnvironmentValue();
Random rand = new Random();
StringBuilder sSql = new StringBuilder();
if (strOperationTable == strServerListTableName)
{
//update ServerList set ServerIP='123.12',ServerBuyTime=GetDate() where ServerID=1
sSql.Append(@" update ServerList ");
sSql.Append(@" set ServerIP='"+ReturnRandomIP(rand)+"', "); //ServerIP
sSql.Append(@" ServerBuyTime=GetDate() "); //ServerBuyTime
sSql.Append(@" where ServerID="+ReturnRandomUpdateOrDeleteID(rand)+" "); //UpdateID
}
else
{
sSql.Append(@" update UserList ");
sSql.Append(@" set UserName = '"+ReturnRandomString(rand)+"', "); //UserName
sSql.Append(@" UserPW = '"+ReturnRandomString(rand)+"' "); //UserPW
sSql.Append(@" where UserID="+ReturnRandomUpdateOrDeleteID(rand)+" "); //UpdateID
}
#endregion

ExecuteOperation(sSql.ToString());

btUpdate.Enabled = true;
}

/// <summary>
/// 添加
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btAdd_Click(object sender, EventArgs e)
{
btAdd.Enabled = false;

#region 初始化
GetEnvironmentValue();
Random rand = new Random();
StringBuilder sSql = new StringBuilder();
if (strOperationTable == strServerListTableName)
{
sSql.Append(@" insert into ServerList (ServerID, ServerIP, ServerBuyTime) ");
sSql.Append(@" values( ");
sSql.Append(@" " + ReturnRandomID(rand) + ", "); //ServerID
sSql.Append(@" '" + ReturnRandomIP(rand) + "', "); //ServerIP
sSql.Append(@" GetDate() "); //ServerBuyTime
sSql.Append(@" ) ");
}
else
{
sSql.Append(@" insert into UserList (UserID, UserName, UserPW) ");
sSql.Append(@" values( ");
sSql.Append(@" " + ReturnRandomID(rand) + ", "); //UserID
sSql.Append(@" '" + ReturnRandomString(rand) + "', "); //UserName
sSql.Append(@" '" + ReturnRandomString(rand) + "' "); //UserPW
sSql.Append(@" ) ");
}
#endregion

ExecuteOperation(sSql.ToString());

btAdd.Enabled = true;
}

/// <summary>
/// 删除
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btDelete_Click(object sender, EventArgs e)
{
btDelete.Enabled = false;

#region 初始化
GetEnvironmentValue();
Random rand = new Random();
StringBuilder sSql = new StringBuilder();
if (strOperationTable == strServerListTableName)
{
sSql.Append(@"delete from ServerList where ServerID="+ReturnRandomUpdateOrDeleteID(rand));
}
else
{
sSql.Append(@"delete from UserList where UserID="+ReturnRandomUpdateOrDeleteID(rand));
}
#endregion

ExecuteOperation(sSql.ToString());

btDelete.Enabled = true;
}
#endregion

}
}

Demo与源码下载:
code.zip (68.69 kb)
release.zip (12.88 kb)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐