找回密码
 注册
搜索
查看: 5453|回复: 0

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

[复制链接]
发表于 2005-9-1 23:30:26 | 显示全部楼层 |阅读模式
  一、ADO简介
1 }5 o6 V6 P1 x% s  |/ ~  tADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。
) g/ d" B4 u2 m/ i本文示例代码
8 _! B5 Y, e1 a& o; G  e/ F% ~3 G: J- Q) ]9 {
二、基本流程 & a9 H" ~8 d% g
万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧!
! o( T/ A8 a/ n, \(1)初始化COM库,引入ADO库定义文件
7 |) I1 G4 C( c0 _6 u- |(2)用Connection对象连接数据库 1 ]: B+ Z* v( O1 `2 {$ x( j2 g
(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。   U, c# U4 N/ F4 s3 Q$ k9 ^# A
(4)使用完毕后关闭连接释放对象。
8 F, R5 ~- Y4 h" N" k8 O
7 j: c" g: Q  ~$ R- b准备工作: $ O$ Q( H( I( p
为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。 - @; u( ^+ ^' z; |
下面我们将详细介绍上述步骤并给出相关代码。
$ Q7 q$ m, L$ R0 L【1】COM库的初始化
# T& V. O6 r1 j7 o* i' E我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码: ; x7 l& R5 H5 [) f! C3 \+ ?- r

/ A8 s( x. k; E. {+ v- i
0 ?0 Y8 f8 q$ l2 t: vBOOL CADOTest1App::InitInstance()   E4 F' l6 p$ `
  {
# p3 X# U. f/ o7 [, ~9 }  AfxOleInit(); . z8 N: U) d( z6 Y% p  ]: W
  ......
% [# q9 N- k# ^* K# l- z- p  r( E: i4 v. |/ ^: b
【2】用#import指令引入ADO类型库
* Q. a! _# Q# e' T我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到)
1 u$ v! h  h' J0 D. K: ^2 }#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF")
* k3 e3 l, H; U) |这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。 2 h5 e6 y& d0 Y
/ V1 E0 c2 q: Z# d) u
几点说明: 0 Q% L: L2 t: G7 p
(1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改 / U( D5 v4 ]  s+ c" \0 U
(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。
# D' `. V. T: S- ^) J7 Mmsado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned
; u' L6 V. V. A. J9 ^+ Y
5 D/ p3 J: C1 r8 i2 {* G【3】创建Connection对象并连接数据库
8 P1 U: D# z& C+ Z9 E: _首先我们需要添加一个指向Connection对象的指针:
- N. h2 Z* G* A$ \0 W- r_ConnectionPtr m_pConnection; 2 r8 h' N! U4 ?' p5 e' r% G
下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。
- \. f' T" f) Q. o' ]' ^) o5 u  i) }  T- M, `) W  Y6 f9 w8 e. b

- j4 }' t1 r% v$ B/ [+ Y: X; ^! N1 BBOOL CADOTest1Dlg::OnInitDialog() & S; D2 ^9 ]3 N$ p, i
  {
3 u5 K9 a/ N* ]1 x  CDialog::OnInitDialog(); * Q0 b% A0 ^0 k, o7 `. l& B- s6 S0 {
  HRESULT hr;
5 h' s& k8 |/ \' q  try
. w4 i# ^2 }' l) f4 S- M  { ! b. x( T* s9 c( x" \. l- ~- H& j
  hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象 8 z" z# P, Z' F1 {2 g9 ?
  if(SUCCEEDED(hr))
+ n3 }% X' S, M  {
, l: U4 T- l5 r. x  hr = m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库
7 X5 y; y9 Z& n8 j# A/ c, ~' N  ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为rovider=Microsoft.Jet.OLEDB.3.51;  } 3 b; Z2 f! q; o' w. d$ D
  } * d1 j0 C5 ~  S2 ^" a2 `3 l3 T! \& q
  catch(_com_error e)///捕捉异常
- J4 r% U' U. {) z  C+ Q  { 8 ^. @% |2 Q( G6 E# Q& m( y
  CString errormessage;   j$ o( J  b. y6 b4 G; D
  errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage()); - Y0 F" `( v0 q) A
  AfxMessageBox(errormessage);///显示错误信息 : @, Q$ w$ n% e; p$ O3 X- ^
  } $ }, Y; R# @3 |5 c

5 G( f, T3 P& [/ ?在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型
- p( O. `/ ~! X& iHRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
0 k7 P! ^* V4 m4 x5 K- NConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权, 6 y" |' |: q! E% B. _* D: |) g. D1 M
Options可以是如下几个常量: # a7 t5 U2 V9 N* P( l" W
adModeUnknown:缺省。当前的许可权未设置 / ^# f4 K( p/ V8 m7 x- ~
adModeRead:只读
- e' A' E$ m+ t- B/ X& t6 DadModeWrite:只写
* J3 r& _0 X$ C" OadModeReadWrite:可以读写
7 U! T6 \/ z$ R# QadModeShareDenyRead:阻止其它Connection对象以读权限打开连接
; j6 h, R. N3 e% w3 c& x3 D0 S- uadModeShareDenyWrite:阻止其它Connection对象以写权限打开连接
! D* \) L8 j, I# V: k8 u: D; F& G, _adModeShareExclusive:阻止其它Connection对象打开连接
: g5 M/ [% f- P6 m) x* RadModeShareDenyNone:允许其它程序或对象以任何权限建立连接
+ W/ `; @$ Q: U( W! s' G- D3 j# ^& C1 _; g# W* w. H$ P
我们给出一些常用的连接方式供大家参考: ! h5 S7 N: u: D4 F# @& S
(1)通过JET数据库引擎对ACCESS2000数据库的连接
5 s1 ~" C5 T) d* S- c
; k# ?, g+ j- R/ @$ B  {m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown);
% `" L2 h: R/ o% L4 C
0 ]) L2 ]1 L; ?6 ?$ l/ {(2)通过DSN数据源对任何支持ODBC的数据库进行连接:
: V1 K; m. g9 ]+ ]. g7 z& Om_pConnection->Open("Data Source=adotest;UID=saWD=;","","",adModeUnknown);
8 u. y; A- H/ I* B: a( O0 O! O% Q
, Y' N, W7 r' I6 |(3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown);
2 z; F# L. v5 u) c  H
$ s  E' K) W* z1 {1 l; Q% r其中Server是SQL服务器的名称,DATABASE是库的名称
' v- b: `; s* q! Q3 _/ o) x
/ P5 V% k/ }# m) q6 c% `5 Z0 |Connection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State 8 t, N2 x% A2 G$ m
ConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒 2 \! i# }6 n9 s4 E
m_pConnection->Open("Data Source=adotest;","","",adModeUnknown); 6 V! Q# ?* o% q# \5 _0 J
5 N, F4 U4 O4 H' A' e6 Y
* T4 ^6 v) |+ c# t* ^8 X+ L& {
State属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如: 7 u1 c0 k! n( P: O
if(m_pConnection->State)
1 |( `- ?% M: Z     m_pConnection->Close(); ///如果已经打开了连接则关闭它
1 W2 I+ e5 g$ Q0 o# o% u6 v7 [( r1 E- Q
+ V; j$ F5 [+ i- B  x5 J% z& G" C
【4】执行SQL命令并取得结果记录集 . ~* e" y6 ^: n, Z
为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset;
  s' p( u- w, N9 N. A; n3 H6 Q5 e并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset"); 6 G$ q1 G3 s, W
SQL命令的执行可以采用多种形式,下面我们一进行阐述。
% [6 d! P* }$ \& ~* U4 c
2 x* |7 r4 s4 h1 E4 [(1)利用Connection对象的Execute方法执行SQL命令
& C- E& V* N7 A% _  C6 \) qExecute方法的原型如下所示: 2 c& E& d/ \2 t% N% A  W
_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一:
9 U" A7 L8 a- b" u$ O  x' ~5 R! {adCmdText:表明CommandText是文本命令
! L$ ]- A1 J3 _+ VadCmdTable:表明CommandText是一个表名 ( g# }& Y( g7 e1 q1 T
adCmdProc:表明CommandText是一个存储过程 5 l0 \& L* d5 J7 R
adCmdUnknown:未知
( m  A" Q8 }1 X3 ^+ ~
+ d7 B+ X$ u9 I. ~Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。   _variant_t RecordsAffected; 0 e) M* L, V. R3 u; |
  ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday
" z1 o0 u1 \% V) g  m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText);
1 |1 ?9 H# A% F" I( J: ~  ///往表格里面添加记录 . D' H% `3 }: [. }
  m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) valueS (1, nullnullnullnullnullnullnullnullWashingtonnullnullnullnullnullnullnullnull,25,nullnullnullnullnullnullnullnull1970/1/1nullnullnullnullnullnullnullnull)",&RecordsAffected,adCmdText);
4 u1 p/ e- j6 x4 s& {7 b% }  ///将所有记录old字段的值加一
2 }* T' k/ v2 i, k1 B. \  m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText); ! j8 X3 ^1 N" {' N. |
  ///执行SQL统计命令得到包含记录条数的记录集
% `: u4 I! C/ _2 N8 F8 u0 U  q  m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);
' c% P1 T. B+ m. M6 x( @/ }  _variant_t vIndex = (long)0; * {0 U' M8 y5 _9 W/ D1 f' F2 M( `
  _variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量 1 \# Y8 Z5 l9 V7 ^0 i
  m_pRecordset->Close();///关闭记录集
9 h% P  S. ^# F/ C, ^  I5 ~  CString message; & Q4 |" E0 s4 N& F! u
  message.Format("共有%d条记录",vCount.lVal); : }8 b) x, ~" c% k% C# r, Y
  AfxMessageBox(message);///显示当前记录条数 - }3 X$ h$ n& \. q: S% y

) L6 P/ M# B3 f$ `' m+ B- G
7 {+ `3 G. r+ b(2)利用Command对象来执行SQL命令 ! A8 b6 a6 r9 {) @
  _CommandPtr m_pCommand; $ P$ a- \* r4 B: N9 t. \
  m_pCommand.CreateInstance("ADODB.Command");
0 G7 ?$ A% Y7 g2 u. c. @  _variant_t vNULL;
) {( r1 S; p% S0 @8 C  vNULL.vt = VT_ERROR; ' a, I) O' I6 e/ F/ x: M
  vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数 - u; O' m4 S  F* U. t. b
  m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它 3 @% d5 E% L( F  Y5 [# k' {
  m_pCommand->CommandText = "SELECT * FROM users";///命令字串
" X4 ~2 R3 D. T* ?$ x& @0 W% L  m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集 $ E! v  u: D# T" ]3 _+ \: Q8 n" m* M

0 g! g. V- e& E7 n5 E$ \6 b8 m- J在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。 8 }. @1 J+ n3 M( ]. H+ b' ^
+ T& O. t8 X: p) Z7 D; T

3 k+ q! N* V* P! q  `(3)直接用Recordset对象进行查询取得记录集
- Q! q% I2 T* e' M例如 ' Q1 K1 d7 t  Y6 y1 N

% ^$ J8 L; r9 M. d' d6 M2 t7 J8 V  m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
, N# U. [1 o( |5 n* h
' a7 p8 t/ y. }; WOpen方法的原型是这样的:
4 g3 o8 h2 @1 h. E, r! ZHRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options )
+ v3 q$ r6 }# r8 N$ h, Q其中:
7 m& h5 \" @" T) o①Source是数据查询字符串
0 W0 P1 Q+ x7 @* y②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象) " Q- p& [! ^4 f& \1 m9 p. L
③CursorType光标类型,它可以是以下值之一,请看这个枚举结构:
) }+ b. G/ r7 v; K9 C- Z/ aenum CursorTypeEnum
, `9 g+ `) E4 L+ z) }5 q{
7 ^9 `$ ?- Y2 HadOpenUnspecified = -1,///不作特别指定 , ]/ Z( y& s; T% |; _% o
adOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用 9 s6 t$ X) F; M& ^3 i
adOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。 : x8 O! Y& F& o9 h
adOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。
" y0 t8 v  e' a% M" ~8 ^adOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。 0 e, [0 [1 O; @1 T
}; ; N! `# I& W. q7 N+ _
④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
5 V# ?4 R" X+ h0 [3 C3 yenum LockTypeEnum # e1 @% I/ w) o9 d; ~) r
{ ' G- G+ Q3 i9 k+ F8 u6 x
adLockUnspecified = -1,///未指定
7 M' \4 P/ F" s! T( g( fadLockReadOnly = 1,///只读记录集
7 r- T+ t; A4 s) jadLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制 5 f5 D! u8 A- a! f' `( \
adLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作 : w! l. g. E( v$ }- ?+ _
adLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。
3 r$ i% O# E! C};
% W& }$ Z: c  I; @" g⑤Options请参考本文中对Connection对象的Execute方法的介绍 3 a' d& o& \% `3 r" p- i! U

2 O6 a; q2 z/ {' f9 j, D+ C: J. p8 e4 h! ]5 j4 Z$ p! @& R
【5】记录集的遍历、更新   t5 v& ]6 h0 b7 F2 x  d
根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday 4 q. E3 Q% Z5 V4 g& v9 X' ?
以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。
! d' O+ q3 T+ [' Q+ L  A/ U# L! G6 ?5 Y2 i/ `, K
/ \1 t0 V9 X3 k
_variant_t vUsername,vBirthday,vID,vOld; 7 k# C* M5 D+ j  T0 m/ Z* q6 M
_RecordsetPtr m_pRecordset; & M" }2 W! O8 x9 e1 K! ?
m_pRecordset.CreateInstance("ADODB.Recordset");
. [& v" Z  A# |4 W( e1 A' ?m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
* p- Y7 ^; I+ ^' jwhile(!m_pRecordset->adoEOF)///这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗? % U+ T( @  o* N, v3 m4 C6 F- O
{
& m9 u- h2 [( E  O7 H0 gvID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行
( w) r4 I/ T1 z- z1 ]) RvUsername = m_pRecordset->GetCollect("username");///取得username字段的值 " w5 \6 d( l! l' o! a( [$ ~! X! B
vOld = m_pRecordset->GetCollect("old");
, }9 c4 H) P7 H$ ?) v, |/ SvBirthday = m_pRecordset->GetCollect("birthday");
' W$ [( c6 w3 c: D- D///在DEBUG方式下的OUTPUT窗口输出记录集中的记录
# G  I; T4 v4 \/ P% q7 ?if(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL)
4 R( {6 T& z0 s  TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday); $ ?7 [5 ~) Y2 {7 N
m_pRecordset->MoveNext();///移到下一条记录
% F* n; P: ?6 Y( P0 [/ N0 ]3 S} : q  F& {. I; n
m_pRecordset->MoveFirst();///移到首条记录
% x' C! I6 J, g/ f+ U2 ?m_pRecordset->Delete(adAffectCurrent);///删除当前记录
) P" A, }# e/ p# ?' ^, m8 _///添加三条新记录并赋值
, ]  m7 P/ U7 C8 m% Z3 M, Ofor(int i=0;i<3;i++)
3 A+ {; ?% A! d" l; j{
; K+ L# e6 H8 X8 V$ `4 _' b1 N6 |m_pRecordset->AddNew();///添加新记录 . k0 S0 W& v; O, J& y
m_pRecordset->PutCollect("ID",_variant_t((long)(i+10)));
4 }$ W/ i# }0 w* o2 X& _# t# v3 Xm_pRecordset->PutCollect("username",_variant_t("叶利钦"));
. w9 y  e1 d+ r6 t/ U" P7 Zm_pRecordset->PutCollect("old",_variant_t((long)71));
9 i) J# o) i2 Y7 g8 hm_pRecordset->PutCollect("birthday",_variant_t("1930-3-15")); ) S9 s/ b7 p6 v$ Y
}
% J; T' N) t8 um_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处
9 c$ M! Z9 W' am_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄 / k* K- a' \& ~9 f% T" e
m_pRecordset->Update();///保存到库中
3 }5 G$ `) z: J
# j% f3 z' j: R4 j- S+ V【6】关闭记录集与连接
$ c' Y- o) B5 v: o$ o" S) j3 j) {0 A记录集或连接都可以用Close方法来关闭
+ [; Z' r  d5 v. z0 b      m_pRecordset->Close();///关闭记录集
; d- J2 I$ F7 b6 r3 v6 j      m_pConnection->Close();///关闭连接
# ~7 K0 U* b6 W. W/ V( O3 B1 n3 A5 w# J2 M# c) v% j" L
至此,我想您已经熟悉了ADO操作数据库的大致流程,也许您已经胸有成竹,也许您还有点胡涂,不要紧!建议你尝试写几个例子,这样会更好地熟悉ADO,最后我给大家写了一个小例子,例子中读出所有记录放到列表控件中、并可以添加、删除、修改记录。 ) \, ?  l( q6 Z3 v& j; w: E
点这里下载示例代码 0 a6 m0 e6 e# D* m

3 Z; Y' g3 L, R# G8 i! l后记:限于篇幅ADO中的许多内容还没有介绍,下次我们将详细介绍Recordset对象的属性、方法并解决几个关键的技术:绑定方式处理记录集数据、存储过程的调用、事务处理、图象在数据库中的保存与读取、与表格控件的配合使用等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2025-12-29 20:07 , Processed in 0.020615 second(s), 15 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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