WCF(10)之WCF對事務的支持

一、引言

  後面還有幾個系列準備些,還有很多東西需要總結的。今天就來介紹下WCF對事務的支持。

二、WCF事務詳解

2.1 事務概念與屬性

   首先,大家在學習數據庫的時候就已經接觸到事務這個概念了。所謂事務,它是一個操作序列,這些操作要麼都執行,要麼都不執行,它是一個不可分割的工作單元。例如,銀行轉賬功能,這個功能涉及兩個邏輯操作

  1. 從一個賬戶A中扣錢

  2. 另一個賬戶B增加對應的錢。

  現實生活中,這兩個操作需要要麼都執行,要麼都不執行。所以在實現轉賬功能時,這兩個操作就可以作爲一個事務來進行提交,這樣才能夠保證轉賬功能的正確執行。

  上面通過銀行轉賬的例子來解釋了事務的概念了,也可以說非常容易理解。然後在數據庫的相關書籍裏面都會介紹事務的特性。一個邏輯工作單元要成爲事務,必須滿足四個特性,這四個特性包括原子性、一致性、隔離性和持久性。這四個特性也簡稱爲ACID(ACID是四個特性英文單詞首字母的縮寫)。

  • 原子性。此屬性可確保特定事務下完成的所有更新都已提交併保持持久,或所有這些更新都已中止並回滾到其先前狀態。

  • 一致性。此屬性可保證某一事務下所做的更改表示從一種一致狀態轉換到另一種一致狀態。例如,將錢從支票帳戶轉移到存款帳戶的事務並不改變整個銀行帳戶中的錢的總額。

  • 隔離。此屬性可防止事務遵循屬於其他併發事務的未提交的更改。隔離在確保一種事務不能對另一事務的執行產生意外的影響的同時,還提供一個抽象的併發。

  • 持久性。這意味着一旦提交對託管資源(如數據庫記錄)的更新,即使出現失敗這些更新也會保持持久。

2.2 事務協議

   WCF支持分佈式事務,也就是說WCF中的事務可以跨越服務邊界、進程、機器和網絡,在多個客戶端和服務之間存在。即WCF中事務可以被傳播的。既然WCF支持事務,則自然就有對應傳輸事務信息的相關協議。所以也就有了事務協議。

   WCF使用不同的事務協議來控制事務的執行範圍,事務協議是爲了實現分佈式環境中事務的傳播。WCF支持以下三種事務協議:

  1. 輕量級協議(Lightweight Protocol):該協議僅用於管理本地環境中的事務,即處於同一應用程序域中的事務。它無法跨越應用程序域的邊界傳播事務,則更不用說跨越進程和機器邊界了。所以Lightweight Protocol只適用於某個服務的內部或外部。但相對於其他協議來說,輕量級協議的性能是最好的,這應該是顯然的,不能跨越進程和機器邊界,則就不存在網絡傳輸。

  2. OleTx協議:該協議用於跨應用程序域、進程和機器邊界傳播事務。協議採用遠程過程調用(RPC),並採用Windows專用的二進制格式。但該協議無法穿越防火牆或與非Windows方協作。所以OleTx協議多用於Windows體系下的內網環境(即Intranet環境)。

  3. WS-Atomic(WS原子性,WSAT)事務協議:該協議與OleTx協議類似,同樣允許事務穿越應用程序域、進程和機器邊界傳播事務。但不同於OleTx協議的是,WSAT協議基於一種行業標準,它使用HTTP協議,並編碼形式爲文本格式,因而可以穿越防火牆。雖然可以在內網中使用WSAT協議,但它主要還是用於Internet環境。

  因爲輕量級協議不能跨越服務邊界傳播事務,所有沒有綁定支持輕量級協議。WCF預定義的綁定中實現了標準的WS-Atomic 協議和Microsoft專有的OleTx協議,我們可以通過編程或配置文件來設置事務協議。具體設置方法如下所示:

 


 

1 <bindings>
2 <netTcpBinding>
3 <!--通過transactionProtocol屬性來設置事務協議-->
4 <binding name="transactionTCP" transactionFlow="true" transactionProtocol="WSAtomicTransactionOctober2004"/>
5 </netTcpBinding>
6 </bindings>
7 // 通過編程設置
8 NetTcpBinding tcpBinding = new NetTcpBinding();
9 // 注意: 事務協議的配置只有在事務傳播的情況下才有意義
10 tcpBinding.TransactionFlow = true;
11 tcpBinding.TransactionProtocol = TransactionProtocol.WSAtomicTransactionOctober2004;

 

這裏需要注意,事務協議的配置只有在允許事務傳播的情況下才有意義。並且NetTcpBinding和NetNamedPipeBinding都提供了TransactionProtocol屬性。由於TCP和IPC綁定只能在內網使用,將它們設置爲WSAT協議並無實際意義,對於WS綁定(如WSHttpBinding、WSDualHttpBinding和WSFederationHttpBinding)並沒有TransactionProtocol屬性,它們設計的目的在於當涉及多個使用WAST協議的事務管理器時,能夠跨越Internet。但如果只有一個事務協調器,OleTx協議將是默認的協議,不必也不能爲它配置一個特殊的協議。

2.3 事務管理器

   分佈式事務的實現要依靠第三方事務管理器來實現。它負責管理一個個事務的執行情況,最後根據全部事務的執行結果,決定提交或回滾整個事務。WCF提供了三個不同的事務管理器,它們分別是輕量級事務管理器(LTM)、核心事務管理器(KTM)和分佈式事務協調器(DTC)。WCF根據平臺使用的公共,應用程序的事務執行的任務、調用的服務以及所消耗的資源分配合適的事務管理器。通過自動地分配事務管理器,WCF將事務管理從服務代碼和用到的事務協議中解耦出來,開發者不必爲事務管理器而苦惱。下面分別介紹下這三種事務管理器。

  • LTM:它只管理本地事務,即在一個單獨應用程序域內的事務。LTM只能管理在一個單獨服務內的事務,該服務不能將事務傳遞給其他服務。LTM在所有的事務管理器中,性能最好。

  • KTM:與LTM一樣,KTM管理的事務只能引入一個服務,並且該服務不得向其他服務傳播事務。

  • DTC:DTC可以管理跨越任意執行邊界的事務,從本地跨越所有的邊界,如進程、機器或站點的邊界。DTC既可以使用OleTx協議,也可以使用WSAT協議。DTC與WCF緊密的集成一起,它是每個運行WCF的機器上默認可用的系統服務,DTC可以創建新的事務、跨機器傳播事務,手機之一管理器的投票並通知資源管理器進行回滾或提交。

2.4 服務支持的4種事務模式

   事務使用哪個事務由綁定的事務流屬性(TransactionFlow屬性)、操作契約中的事務流選項(TransactionFlowOption) 以及操作行爲特性中的事務範圍屬性(TransactionScopeRequired)共同決定。TransactionFlow屬性有2個值,true 或false,TransactionFlowOption有三個值,NotAllowed、Allowed和Mandatory,TransactionScopeRequired有兩個值,true或false。所以一共有12種(2*3*2)可能的配置設置。在這些配置設置中,有4種不滿足要求的,例如在綁定中設置TransactionFlow屬性爲false,卻設置TransactionFlowOption爲Mandatory。下圖列出了剩下的8種情況:

  上圖中的8中排列組合實際最終只產生了四種事務傳播模式,這4種傳播模式爲:Client/Service、Client、Service和None。上圖黑體字指出各種模式推薦的配置設置。在設計應用程序時,每種模式都有它自己的適用場景。對於除None模式的其他三種模式的推薦配置詳細介紹如下所示:

  • Client/Service:最常見的一種事務模型,通常由客戶端或服務本身啓用一個事務。設置步驟:

  (1) 選擇一個支持事務的Binding,設置 TransactionFlow = true。
  (2) 設置 TransactionFlow(TransactionFlowOption.Allowed)。
  (3) 設置 OperationBehavior(TransactionScopeRequired=true)。

  • Client:強制服務必須參與事務,而且必須是客戶端啓用事務。設置步驟:

     (1) 選擇一個支持事務的Binding,設置 TransactionFlow = true。
     (2) 設置 TransactionFlow(TransactionFlowOption.Mandatory)。
     (3) 設置 OperationBehavior(TransactionScopeRequired=true)。

  • Service:服務必須啓用一個根事務,且不參與任何外部事務。設置步驟:

     (1) 選擇任何一種Binding,設置 TransactionFlow = false(默認)。
     (2) 設置 TransactionFlow(TransactionFlowOption.NotAllowed)。
     (3) 設置 OperationBehavior(TransactionScopeRequired=true)。

 三、WCF事務服務的實現

  上面內容對WCF中事務進行了一個詳細的介紹,下面具體通過一個實例來說明WCF中如何實現對事務的支持。首先還是按照前面博文中介紹的步驟來實現該實例。

  第一步:創建WCF契約和契約的實現,具體的實現代碼如下所示:

 


 

namespace WCFContractAndService
{
// 服務契約
[ServiceContract(SessionMode= SessionMode.Required)]
//[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false)]
public interface IOrderService
{
// 操作契約
[OperationContract]
// 控制客戶端的事務是否傳播到服務
// TransactionFlow的值會包含在服務發佈的元數據上
[TransactionFlow(TransactionFlowOption.NotAllowed)]
List<Customer> GetCustomers();

[OperationContract]
[TransactionFlow(TransactionFlowOption.NotAllowed)]
List<Product> GetProducts();

[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
string PlaceOrder(Order order);

[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
string AdjustInventory(int productId, int quantity);

[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
string AdjustBalance(int customerId, decimal amount);
}

[DataContract]
public class Customer
{
[DataMember]
public int CustomerId { get; set; }

[DataMember]
public string CompanyName { get; set; }

[DataMember]
public decimal Balance { get; set; }
}

[DataContract]
public class Product
{
[DataMember]
public int ProductId { get; set; }

[DataMember]
public string ProductName { get; set; }

[DataMember]
public decimal Price { get; set; }

[DataMember]
public int OnHand { get; set; }
}

[DataContract]
public class Order
{
[DataMember]
public int CustomerId { get; set; }

[DataMember]
public int ProductId { get; set; }

[DataMember]
public decimal Price { get; set; }

[DataMember]
public int Quantity { get; set; }

[DataMember]
public decimal Amount { get; set; }
}
}

namespace WCFContractAndService
{
// 服務實現
[ServiceBehavior(
TransactionIsolationLevel = IsolationLevel.Serializable,
TransactionTimeout= "00:00:30",
InstanceContextMode = InstanceContextMode.PerSession,
TransactionAutoCompleteOnSessionClose = true)]
public class OrderService :IOrderService
{
private List<Customer> customers = null;
private List<Product> products = null;
private int orderId = 0;
private string conString = Properties.Settings.Default.TransactionsConnectionString;

public List<Customer> GetCustomers()
{

customers = new List<Customer>();
using (var cnn = new SqlConnection(conString))
{
using (var cmd = new SqlCommand("SELECT * " + "FROM Customers ORDER BY CustomerId", cnn))
{
cnn.Open();
using (SqlDataReader CustomersReader = cmd.ExecuteReader())
{
while (CustomersReader.Read())
{
var customer = new Customer();
customer.CustomerId = CustomersReader.GetInt32(0);
customer.CompanyName = CustomersReader.GetString(1);
customer.Balance = CustomersReader.GetDecimal(2);
customers.Add(customer);
}
}
}
}

return customers;
}

public List<Product> GetProducts()
{
products = new List<Product>();
using (var cnn = new SqlConnection(conString))
{
using (var cmd = new SqlCommand(
"SELECT * " +
"FROM Products ORDER BY ProductId", cnn))
{
cnn.Open();
using (SqlDataReader productsReader =
cmd.ExecuteReader())
{
while (productsReader.Read())
{
var product = new Product();
product.ProductId = productsReader.GetInt32(0);
product.ProductName = productsReader.GetString(1);
product.Price = productsReader.GetDecimal(2);
product.OnHand = productsReader.GetInt16(3);
products.Add(product);
}
}
}
}
return products;
}

// 設置服務的環境事務
// 使用Client模式,即使用客戶端的事務
[OperationBehavior(TransactionScopeRequired =true, TransactionAutoComplete = false)]
public string PlaceOrder(Order order)
{
using (var conn = new SqlConnection(conString))
{
var cmd = new SqlCommand(
"Insert Orders (CustomerId, ProductId, " +
"Quantity, Price, Amount) " + "Values( " +
"@customerId, @productId, @quantity, " +
"@price, @amount)", conn);

cmd.Parameters.Add(new SqlParameter(
"@customerId", order.CustomerId));
cmd.Parameters.Add(new SqlParameter(
"@productid", order.ProductId));
cmd.Parameters.Add(new SqlParameter(
"@price", order.Price));
cmd.Parameters.Add(new SqlParameter(
"@quantity", order.Quantity));
cmd.Parameters.Add(new SqlParameter(
"@amount", order.Amount));

try
{
conn.Open();
if (cmd.ExecuteNonQuery() <= 0)
{
return "The order was not placed";
}

cmd = new SqlCommand(
"Select Max(OrderId) From Orders " +
"Where CustomerId = @customerId", conn);
cmd.Parameters.Add(new SqlParameter(
"@customerId", order.CustomerId));

using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
orderId = Convert.ToInt32(reader[0].ToString());
}
}
return string.Format("Order {0} was placed", orderId);
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
}

// 使用Client模式,即使用客戶端的事務
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
public string AdjustInventory(int productId, int quantity)
{
using (var conn = new SqlConnection(conString))
{
var cmd = new SqlCommand(
"Update Products Set OnHand = " +
"OnHand - @quantity " +
"Where ProductId = @productId", conn);
cmd.Parameters.Add(new SqlParameter(
"@quantity", quantity));
cmd.Parameters.Add(new SqlParameter(
"@productid", productId));

try
{
conn.Open();
if (cmd.ExecuteNonQuery() <= 0)
{
return "The inventory was not updated";
}
else
{
return "The inventory was updated";
}
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
}

// 使用Client模式,即使用客戶端的事務
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
public string AdjustBalance(int customerId, decimal amount)
{
using (var conn = new SqlConnection(conString))
{
var cmd = new SqlCommand(
"Update Customers Set Balance = " +
"Balance - @amount " +
"Where CustomerId = @customerId", conn);
cmd.Parameters.Add(new SqlParameter(
"@amount", amount));
cmd.Parameters.Add(new SqlParameter(
"@customerId", customerId));

try
{
conn.Open();
if (cmd.ExecuteNonQuery() <= 0)
{
return "The balance was not updated";
}
else
{
return "The balance was updated";
}
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
}
}
}

 

  上面的服務契約和服務實現與傳統的實現沒什麼區別。這裏使用IIS來宿主WCF服務。

  第二步:宿主的實現。創建一個空的Web的項目,並添加WCF服務文件,具體內容如下所示:


 

<%@ ServiceHost Language="C#" Debug="true" Service="WCFContractAndService.OrderService" CodeBehind="OrdersService.svc.cs" %>

  對應的Web.config的內容如下所示:

 


 

<configuration>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>

<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="OrderServiceBehavior">

<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<wsHttpBinding>
<!--通過設置transactionFlow屬性爲true來使綁定支持事務傳播;對於wsHttpBinding契約事務傳播-->
<binding name="wsHttpBinding" transactionFlow="true">
<!--啓用消息可靠性選項-->
<!--<reliableSession enabled="true"/>-->
</binding>

</wsHttpBinding>

</bindings>
<services>
<service name="WCFContractAndService.OrderService" behaviorConfiguration="OrderServiceBehavior">
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsHttpBinding" contract="WCFContractAndService.IOrderService"/>
</service>
</services>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
</system.serviceModel>
</configuration>

 

  這裏採用了wsHttpBinding綁定,並設置其transactionFlow屬性爲true使其支持事務傳播。接下來看看客戶端的實現。

  第三步:WCF客戶端的實現,通過添加服務引用的方式來生成代理類。這裏的客戶端是WinForm程序。

 


 

1 public partial class Form1 : Form
2 {
3 public Form1()
4 {
5 InitializeComponent();
6 }
7
8 private Customer customer = null;
9 private List<Customer> customers = null;
10 private Product product = null;
11 private List<Product> products = null;
12 private OrderServiceClient proxy = null;
13 private Order order = null;
14 private string result = String.Empty;
15
16 private void Form1_Load(object sender, EventArgs e)
17 {
18 proxy = new OrderServiceClient("WSHttpBinding_IOrderService");
19 GetCustomersAndProducts();
20 }
21
22 private void GetCustomersAndProducts()
23 {
24 customers = proxy.GetCustomers().ToList<Customer>();
25 customerBindingSource.DataSource = customers;
26
27 products = proxy.GetProducts().ToList<Product>();
28 productBindingSource.DataSource = products;
29 }
30
31 private void placeOrderButton_Click(object sender, EventArgs e)
32 {
33 customer = (Customer)this.customerBindingSource.Current;
34 product = (Product)this.productBindingSource.Current;
35 Int32 quantity = Convert.ToInt32(quantityTextBox.Text);
36
37 order = new Order();
38 order.CustomerId = customer.CustomerId;
39 order.ProductId = product.ProductId;
40 order.Price = product.Price;
41 order.Quantity = quantity;
42 order.Amount = order.Price * Convert.ToDecimal(order.Quantity);
43
44 // 事務處理
45 using (var tranScope = new TransactionScope())
46 {
47 proxy = new OrderServiceClient("WSHttpBinding_IOrderService");
48 {
49 try
50 {
51 result = proxy.PlaceOrder(order);
52 MessageBox.Show(result);
53
54 result = proxy.AdjustInventory(product.ProductId, quantity);
55 MessageBox.Show(result);
56
57 result = proxy.AdjustBalance(customer.CustomerId,
58 Convert.ToDecimal(quantity) * order.Price);
59 MessageBox.Show(result);
60
61 proxy.Close();
62 tranScope.Complete(); // Cmmmit transaction
63 }
64 catch (FaultException faultEx)
65 {
66 MessageBox.Show(faultEx.Message +
67 "\n\nThe order was not placed");
68
69 }
70 catch (ProtocolException protocolEx)
71 {
72 MessageBox.Show(protocolEx.Message +
73 "\n\nThe order was not placed");
74 }
75 }
76 }
77
78 // 成功提交後強制刷新界面
79 quantityTextBox.Clear();
80 try
81 {
82 proxy = new OrderServiceClient("WSHttpBinding_IOrderService");
83 GetCustomersAndProducts();
84 }
85 catch (FaultException faultEx)
86 {
87 MessageBox.Show(faultEx.Message);
88 }
89 }
90 }

 

  從上面代碼可以看出,WCF事務的實現是利用TransactionScope事務類來完成的。下面讓我們看看程序的運行結果。在運行程序之前,我們必須運行SQL腳本來創建程序中的使用的數據庫,具體的腳本如下所示:

 


 

1 USE [TransactionsDemo]
2 GO
3 /****** Object: Table [dbo].[Customers] Script Date: 01/15/2009 08:14:25 ******/
4 SET ANSI_NULLS ON
5 GO
6 SET QUOTED_IDENTIFIER ON
7 GO
8 CREATE TABLE [dbo].[Customers](
9 [CustomerId] [int] IDENTITY(1,1) NOT NULL,
10 [Name] [nvarchar](20) NOT NULL,
11 [Balance] [smallmoney] NOT NULL, check(Balance >= 0),
12 CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
13 (
14 [CustomerId] ASC
15 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
16 ) ON [PRIMARY]
17 GO
18 /****** Object: Table [dbo].[Products] Script Date: 01/15/2009 08:14:25 ******/
19 SET ANSI_NULLS ON
20 GO
21 SET QUOTED_IDENTIFIER ON
22 GO
23 CREATE TABLE [dbo].[Products](
24 [ProductId] [int] IDENTITY(1,1) NOT NULL,
25 [Name] [nvarchar](20) NOT NULL,
26 [Price] [smallmoney] NOT NULL,
27 [OnHand] [smallint] NOT NULL, check(OnHand >= 0),
28 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED
29 (
30 [ProductId] ASC
31 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
32 ) ON [PRIMARY]
33 GO
34 /****** Object: Table [dbo].[Orders] Script Date: 01/15/2009 08:14:25 ******/
35 SET ANSI_NULLS ON
36 GO
37 SET QUOTED_IDENTIFIER ON
38 GO
39 CREATE TABLE [dbo].[Orders](
40 [OrderId] [int] IDENTITY(1,1) NOT NULL,
41 [CustomerId] [int] NOT NULL,
42 [ProductId] [int] NOT NULL,
43 [Quantity] [smallint] NOT NULL,
44 [Price] [smallmoney] NOT NULL,
45 [Amount] [smallmoney] NOT NULL,
46 CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED
47 (
48 [OrderId] ASC
49 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
50 ) ON [PRIMARY]
51 GO
52 INSERT Customers (Name, Balance) VALUES ('Contoso', 10000)
53 INSERT Customers (Name, Balance) VALUES ('Northwind', 25000)
54 INSERT Customers (Name, Balance) VALUES ('Litware', 50000)
55 INSERT Products (Name, Price, OnHand) VALUES ('Wood', 100, 1000)
56 INSERT Products (Name, Price, OnHand) VALUES ('Wallboard', 200, 2500)
57 INSERT Products (Name, Price, OnHand) VALUES ('Pipe', 500, 5000)
58 GO

 

  生成程序使用的數據庫之後,按F5運行WCF客戶端程序,並在出現的界面中購買Wood材料100,運行結果如下圖所示:

  單擊Place order按鈕後,即執行下訂單操作,如果訂單成功後,將會更新產品的庫存和用戶的餘額,你將看到如下圖所示的運行結果:

四、小結

   到這裏,關於WCF中事務的介紹就結束了。WCF支持四種事務模式,Client/Service、Client、Service和None,對於每種模式都有其不同的配置。一般儘量使用Client/Service或Client事務模式。WCF事務的實現藉助於已有的System.Transaction實現本地事務的編程,而分佈式事務則藉助MSDTC分佈式事務協調機制來實現。WCF提供了支持事務傳播的綁定協議包括:wsHttpBinding、WSDualHttpBinding、WSFederationBinding、NetTcpBinding和NetNamedPipeBinding,最後兩個綁定允許選擇WS-AT協議或OleTx協議,而其他綁定都使用標準的WS-AT協議。在一一篇博文將分享WCF對消息隊列的支持。

 

轉自:https://www.cnblogs.com/zhili/p/MSMQ.html,作者:Learning hard。

如有侵權,請聯繫我刪除!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章