找回密码
 注册
查看: 5737|回复: 0

[收藏]VC++ADO连接数据库

[复制链接]
发表于 2005-9-1 23:30:26 | 显示全部楼层 |阅读模式
  一、ADO简介
( L2 {' c7 E/ s9 f6 \ADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。
& @! W- K+ f/ X* L本文示例代码 ! g3 T9 U# b' }2 l

( X9 Y: f9 c/ v5 ~0 b& W二、基本流程 6 F7 w2 ]( C/ z- Y( h4 v
万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧! + f% L/ `$ f" T" @) y8 \) G2 T
(1)初始化COM库,引入ADO库定义文件
% C( e/ _% d2 a* s(2)用Connection对象连接数据库
; V+ B+ E/ e4 R* F  N(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。
) W! I  Z: y% Q. M- _: E(4)使用完毕后关闭连接释放对象。 6 P7 w# C1 }: u' l$ L; a5 G/ W

) d6 ?% e6 \. A! q6 U准备工作:
2 X6 K' f( k; N% D为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。 ) \: O" ?# z9 {9 p  ]
下面我们将详细介绍上述步骤并给出相关代码。 & P! t/ T3 Z9 @( y% O
【1】COM库的初始化 : M$ Z/ B2 Y0 ^7 M& A
我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码:
. p' n0 j, a. A8 J) ^
: S2 r5 w% r8 I7 q$ n4 [* P; w- c3 W& ]- [" c; j
BOOL CADOTest1App::InitInstance() * v, k: a: y7 Z' d# b: v* c
  { 7 |/ I' t, Y5 U# r: a
  AfxOleInit(); - W1 X8 M9 n  q
  ......
2 |$ F, G0 L6 A/ Z! }" s" S+ _& _: h* d8 w  |6 k! x
【2】用#import指令引入ADO类型库
, C  Q/ I* ]  Y2 G# _" e我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到) ( }9 V/ c: x% S" J/ {
#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF")
; w1 o& f/ o2 s7 }" |8 {: f这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。 4 x- c1 i2 u! [' j. R3 Q: y7 W

) G: {4 b0 A! L1 m: p$ i1 Q; Y几点说明:   t" y' c4 ~; ]! O+ w
(1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改
- B0 s0 {) W) W& ^" P' I% a(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。
  H3 V3 B+ ~, `% s6 dmsado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned
5 l2 Q1 S" y1 t' t' E: e, a; P& `5 [# u& y- p6 m
【3】创建Connection对象并连接数据库 : q3 G) J9 `& D
首先我们需要添加一个指向Connection对象的指针:
# R3 e0 y1 N4 G  N; E. {- M_ConnectionPtr m_pConnection;
+ E. g6 b* a6 S: y' [' d下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。
3 H! {9 I3 x/ b& c/ {" E2 C9 E& t# M) N
. U0 r, L( t& _) G$ r
+ f6 D1 W( T. ?/ lBOOL CADOTest1Dlg::OnInitDialog()
$ D! ?6 G7 A+ J1 ]  {
' c; @; y2 L" S' g5 h/ \1 `  CDialog::OnInitDialog();
3 i! g3 {. S! I/ s* A  HRESULT hr; & X$ _1 \1 Z8 Q+ V+ P
  try 1 Q0 ]. G+ A* U
  { & `. j" Z6 e3 A5 i! O" F6 K! z0 l
  hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象
4 h, r- ^3 k* p1 N+ @0 J$ J# C4 W  if(SUCCEEDED(hr))
( m' y, c1 v+ S3 l4 r  { 5 s7 q" q; S' l* W& D& g
  hr = m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库
& r5 J; x% f% k- [5 |  ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为rovider=Microsoft.Jet.OLEDB.3.51;  }
! B% F  q' T( c( G2 C  }
( T% N  `: b$ @  G* `& }- t1 D( m  catch(_com_error e)///捕捉异常
9 U6 B9 b2 q( v7 e' a5 z% s  { $ U, q" ~" B& `4 t1 S
  CString errormessage; # E0 F% ^. f0 Y# P7 E. m5 S7 G
  errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage());   F( e3 {% X+ V5 }6 _
  AfxMessageBox(errormessage);///显示错误信息 0 l3 H# @: V9 D9 D
  }
' h% n" t% I% c* d: Y! L/ P& o6 }4 ?; J" {
在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型 : I& V2 f6 Q; H# E: X2 L
HRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
- l5 p5 e$ x8 H- HConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权,   M4 z- i4 B, R. _0 t' |1 P
Options可以是如下几个常量:
6 y0 `: f3 |/ P" Z) W/ vadModeUnknown:缺省。当前的许可权未设置
& Z" g2 F" t6 LadModeRead:只读 9 s3 s$ N& Z: O3 i& N2 \9 _. S
adModeWrite:只写
" a4 {4 u$ j0 X: K( RadModeReadWrite:可以读写
8 @( U. P- v2 b/ B. tadModeShareDenyRead:阻止其它Connection对象以读权限打开连接
" Z: F& a; r2 V3 x+ ]- `adModeShareDenyWrite:阻止其它Connection对象以写权限打开连接 4 `. U; W; s/ u3 K5 B' |$ V
adModeShareExclusive:阻止其它Connection对象打开连接 3 L( X1 q! M6 ~" ^
adModeShareDenyNone:允许其它程序或对象以任何权限建立连接 : A# g0 Z, u1 f$ q

; O5 C# [; ~. A2 ]4 ~0 \9 y我们给出一些常用的连接方式供大家参考:
* N: \) z1 N" e! e# I5 O* x8 }(1)通过JET数据库引擎对ACCESS2000数据库的连接 8 X( m$ w+ q0 `( X4 ?

) X6 A% u: d1 q, ^1 H; u" X; em_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown); , v0 @* X5 T+ k& B, ~5 N% M7 L
8 _7 h0 T  i: `1 P
(2)通过DSN数据源对任何支持ODBC的数据库进行连接: ; ]9 d( W/ v& V/ T" Z4 v. d
m_pConnection->Open("Data Source=adotest;UID=saWD=;","","",adModeUnknown);
* c! I" c4 `; {0 d& u+ {+ t+ W
) L$ x! ]! W+ c(3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown);
5 u- ?. g+ f. m1 n" f: }$ u0 c( K1 a' ]8 z4 X0 @, p
其中Server是SQL服务器的名称,DATABASE是库的名称 8 P% f7 G( h1 ]
, B6 X; U1 \9 |3 y+ e- z8 _
Connection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State ! C0 ?  s* L( h6 m6 I
ConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒
: \8 H+ h9 H" d! k" B" cm_pConnection->Open("Data Source=adotest;","","",adModeUnknown);   |: S6 \( x, R, h. g* n; J! l
. N" Z) f+ S) x8 S' S

( y. Z, q+ ?2 ^# t' O6 H. nState属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如: 6 ?7 v7 W- G% P
if(m_pConnection->State)
/ f/ \& g. d: p# R     m_pConnection->Close(); ///如果已经打开了连接则关闭它 3 n% A7 c* W9 b# Y* z, t$ N" D/ E

6 F- N$ s, c5 F) U" j2 C" n3 E2 O+ ]+ |9 f
【4】执行SQL命令并取得结果记录集
4 _/ a0 s/ R! a8 R2 R7 K- Q' Y* a为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset; ( X( P! v9 Z: J8 ]+ {+ |
并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset"); * J# e+ u0 f- U" L
SQL命令的执行可以采用多种形式,下面我们一进行阐述。
( L3 e3 o1 B1 S$ D4 C# Q' e5 M- X' _$ [; b+ t& U' Y6 U9 t8 ^
(1)利用Connection对象的Execute方法执行SQL命令
  @+ i/ e6 S6 m1 _; PExecute方法的原型如下所示: 6 N: C& |5 \! n+ i) a; d
_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一:
% k2 w+ E6 ^" k) |adCmdText:表明CommandText是文本命令 8 m* O1 p% E* |; t! O3 l6 Z1 Y
adCmdTable:表明CommandText是一个表名
: {3 }  J" G! XadCmdProc:表明CommandText是一个存储过程
  q2 o5 G, U3 [+ yadCmdUnknown:未知 2 P1 J9 m; e2 \& H; s

- ?% Z% }4 n9 `5 hExecute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。   _variant_t RecordsAffected;
  {% c" |/ o+ ]3 X5 k7 N4 I& t  ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday + c3 P! O( Q; L) Y$ y$ ^8 ]) w
  m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText);
: J3 N  E+ v: \2 \  ///往表格里面添加记录 9 R5 M4 B- T. g" u8 Z
  m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) valueS (1, nullnullnullnullnullnullnullnullWashingtonnullnullnullnullnullnullnullnull,25,nullnullnullnullnullnullnullnull1970/1/1nullnullnullnullnullnullnullnull)",&RecordsAffected,adCmdText);
7 b+ d, d; g" e3 g% e, e5 N  ///将所有记录old字段的值加一 # }  y( J# Y' u; `2 i! C- _
  m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText); 0 P, U: V& y1 b+ c& a
  ///执行SQL统计命令得到包含记录条数的记录集 " a" F9 x% k+ X# h: w# u) {
  m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);
! S# d; S. J) t. T: s' q' J  E( H  _variant_t vIndex = (long)0; 5 w7 r, f5 d4 R
  _variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量
4 k# ^* ~; |7 e' j, R  m_pRecordset->Close();///关闭记录集 5 }+ q+ s; R$ D# v; n
  CString message; - ^0 L' _9 R* ]6 Q4 C3 `& v* {
  message.Format("共有%d条记录",vCount.lVal);
+ [) S5 E8 a4 |9 T8 V* D7 ?! v5 U  AfxMessageBox(message);///显示当前记录条数
0 D  k9 e) B: e5 b
. s6 H0 x" K# G. J
- e8 m, [* Y2 l(2)利用Command对象来执行SQL命令
2 ]1 W# `! F$ Z) w  _CommandPtr m_pCommand;   P6 ]4 ]( R) J* S( {1 A
  m_pCommand.CreateInstance("ADODB.Command");
' p9 ^; ]  _. I( f! O; Q& D6 C  _variant_t vNULL;   ^/ O" I- k; D0 ^
  vNULL.vt = VT_ERROR;
+ ^( W) _4 y9 ]3 z9 n( n" j2 u  vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数
+ W$ {( v6 J% {- a! n* d  N6 D7 R# }  m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它
  N) `5 `7 e, Q% y. X  m_pCommand->CommandText = "SELECT * FROM users";///命令字串 ' E9 h# `* t' Y9 z# l* I, B1 e7 P
  m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集 ) K& R: v( @2 w
# _+ ]5 t3 M6 ?# N9 n5 M
在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。 & V  H1 ?/ L) H+ Y9 l% m  ~! ~8 D
8 q: U- m: {4 m' u- q" _; V: t; ~3 U

) X6 D/ Q0 o% S5 o. M( e(3)直接用Recordset对象进行查询取得记录集
& A% x  h0 m+ Y; Z' [* Z例如
0 F" S' k' c0 ?
7 L6 l5 c( N; u  m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText); / |- n$ S% K- d+ z* b
# P7 M- a$ L% c+ K9 w/ S/ W" P6 Y
Open方法的原型是这样的:
% x, s8 f9 S% G  i  K6 c5 R" B' ZHRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options ) # q. Q9 b+ {3 y7 T6 F8 \7 O# v
其中: 7 w0 J& e  j" n
①Source是数据查询字符串 8 d; |: e  Y2 A1 }8 R0 Q2 y& z& y
②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象) & M6 ^& \' r" v7 D
③CursorType光标类型,它可以是以下值之一,请看这个枚举结构: # c  T# b# J3 r% t* B! p" P
enum CursorTypeEnum + ?9 b  F  z! P# [9 W) p
{ - p% k. B) A# c$ O
adOpenUnspecified = -1,///不作特别指定 4 C- G9 @7 c8 A5 C( y9 A8 q( m
adOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用 & t* Q* G4 M/ K3 A+ B" F! ~% i- k
adOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。 " p8 a* k5 j* z
adOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。 8 m; }5 w2 f" Z
adOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。 5 n' H- D% C- {& s; W: \9 b
}; - G' g7 W) i, l
④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
. C8 u/ P9 n% B; Zenum LockTypeEnum . F& q& `  @/ Y6 q- b& g/ w
{ + u5 a/ d( ]. t) Q& K+ l% P; t5 u
adLockUnspecified = -1,///未指定 2 w* P4 y# r3 u$ g+ [/ {
adLockReadOnly = 1,///只读记录集
9 q" {7 x& ~) O7 L1 q# X8 u2 {adLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制   o5 V4 v* s5 Y8 X& [
adLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作 9 P$ ^" @+ |/ b$ w
adLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。
! [7 \) o) A: d/ p+ z* Q% p}; & T7 m2 ~8 {  i( ~+ W8 n
⑤Options请参考本文中对Connection对象的Execute方法的介绍
, d0 C" V+ q5 O9 E: Z5 r& v8 f. ]3 o9 [) y0 {7 ?

; r& |+ S* V9 Q1 J, a) {【5】记录集的遍历、更新
& A$ m0 V4 E  [# Y/ R根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday % @/ ]2 A( w2 [( {6 i3 `& ]
以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。
) T; ^; t7 c5 A  S( A  _9 D* F
& H$ I  L9 W5 p# J8 B, P9 w) S9 P3 U  Q/ d5 V
_variant_t vUsername,vBirthday,vID,vOld; / |# t& i% N5 F7 m! E# O
_RecordsetPtr m_pRecordset; 6 {+ @3 A. c$ ~; E+ v' W+ H8 D
m_pRecordset.CreateInstance("ADODB.Recordset");
+ m  J5 V; b+ G0 z8 fm_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText); 6 H7 F* Z& C6 G' s, `
while(!m_pRecordset->adoEOF)///这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗?
5 q+ X, s" U  N# r* t+ |{ & ^5 A) I6 W8 C1 ?' k
vID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行 , l6 ?+ g' h, J" p3 l
vUsername = m_pRecordset->GetCollect("username");///取得username字段的值 ' V9 }" u- A  ?9 d6 E
vOld = m_pRecordset->GetCollect("old");
, d# K, @/ B0 {, uvBirthday = m_pRecordset->GetCollect("birthday");
4 N7 J, \; x. s8 J( I, y  h///在DEBUG方式下的OUTPUT窗口输出记录集中的记录
/ k/ H2 }; H8 w" [if(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL)
0 v7 N/ `# Y/ E& }0 m9 k+ K  TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday);
: |2 y- l" _. Y4 r& r1 }" Im_pRecordset->MoveNext();///移到下一条记录
0 M- @) Q: G. u1 q- g+ @1 ?! B% p9 Z}
/ q( ~; z6 `/ Wm_pRecordset->MoveFirst();///移到首条记录
+ `+ x6 Z: T( W! M- v/ p" lm_pRecordset->Delete(adAffectCurrent);///删除当前记录 " O5 f/ S' ~+ Q8 w$ s
///添加三条新记录并赋值 " k% S& w, G6 n2 U0 v% u9 o
for(int i=0;i<3;i++)
+ \2 j) l# R" Z, W{
* c+ ~& r( F/ t" P% [9 ]m_pRecordset->AddNew();///添加新记录 0 K$ v% F  a' e
m_pRecordset->PutCollect("ID",_variant_t((long)(i+10))); ) {% m3 Q. }' X
m_pRecordset->PutCollect("username",_variant_t("叶利钦")); ) E! ~1 {# }: P
m_pRecordset->PutCollect("old",_variant_t((long)71));
% G: i( K+ `/ f9 k1 m: h" d- cm_pRecordset->PutCollect("birthday",_variant_t("1930-3-15"));
" |5 O+ o4 p; k, S' W0 r" |}
/ X4 x8 U7 Y" A5 H" v; j! ^+ im_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处 4 f: O! o5 C- Z* N% y# H5 ?( Z
m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄
6 I/ B6 [- q8 B( Lm_pRecordset->Update();///保存到库中 # B, n* d) `. ?: Y! v. G" m- K

" O" d7 d- s# f  a3 h1 q【6】关闭记录集与连接 2 B- u% P0 y4 B% X
记录集或连接都可以用Close方法来关闭   A/ k0 n0 T2 a9 \, w- D# p( ~
     m_pRecordset->Close();///关闭记录集 % H( u! N# M+ [
     m_pConnection->Close();///关闭连接 6 X, @- c8 z# D0 x  u2 q& G3 N6 v
7 x$ o9 d. z/ S, v3 s0 i* ?" T- h& |
至此,我想您已经熟悉了ADO操作数据库的大致流程,也许您已经胸有成竹,也许您还有点胡涂,不要紧!建议你尝试写几个例子,这样会更好地熟悉ADO,最后我给大家写了一个小例子,例子中读出所有记录放到列表控件中、并可以添加、删除、修改记录。
- D, C" ^5 T* F) Y! t/ G点这里下载示例代码 8 |; I& _$ U! Z8 e& J; |
! X$ C/ x. s- P
后记:限于篇幅ADO中的许多内容还没有介绍,下次我们将详细介绍Recordset对象的属性、方法并解决几个关键的技术:绑定方式处理记录集数据、存储过程的调用、事务处理、图象在数据库中的保存与读取、与表格控件的配合使用等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|宁德市腾云网络科技有限公司 ( 闽ICP备2022007940号-5|闽公网安备 35092202000206号 )

GMT+8, 2026-10-1 20:51 , Processed in 0.021542 second(s), 14 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表