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

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

[复制链接]
发表于 2005-9-1 23:30:26 | 显示全部楼层 |阅读模式
  一、ADO简介 * r$ G$ i* S9 m! K) d+ t3 y
ADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。 ! c# t0 A2 i# Y1 |; u
本文示例代码 $ \$ v, ^, I: q3 q# P, _; i

! J' T$ c  a2 D4 Y8 @二、基本流程
5 o9 P5 L! \5 \2 O. Y  b万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧! 6 o8 A' ?, K/ e' t1 i# X0 b. t
(1)初始化COM库,引入ADO库定义文件
! k% d- D0 w2 V4 r: q0 C(2)用Connection对象连接数据库
- J; Q, v. f# A6 P6 @: Y1 c(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。
. i, V+ }/ ]0 O; Q" n- s# q8 I3 Z(4)使用完毕后关闭连接释放对象。
7 ]4 q3 j$ t  t! i. A+ |
# l( r; l) p9 @准备工作:
! v2 K! t; P* T4 Q为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。 ; r+ @! q1 |: A7 I2 u* F8 ^! K
下面我们将详细介绍上述步骤并给出相关代码。 + V; W5 B. D. n  V
【1】COM库的初始化
- O3 Z" c. A$ ?7 R, u* c0 e我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码: ' Z% y# w" w) ^4 J; Y
0 c. r( J8 l' f* ^7 I

2 l& A# w4 ^( TBOOL CADOTest1App::InitInstance() / _9 T  X: N3 F* g  R& Q
  { % m# L1 Y# b/ b+ v7 x$ d0 _" L, D
  AfxOleInit();
* t5 E; w) t7 ^! L; O. F+ _/ i, P  ...... 2 D- G$ z/ ?8 w7 v5 @% I5 ~, `
3 B! l! L& c, n; S+ }
【2】用#import指令引入ADO类型库 . M! w: {; w( J& B7 B, f- a
我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到)
- {2 ?% D& G7 O. s#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF") - p4 u  Q* q$ y! O1 D, I6 y* l
这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。 ! A: o' _9 T# M1 a* U+ p
* j5 W' y0 Q0 o
几点说明: & f! {1 W6 a- g* [0 K" `' y
(1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改
! p/ C# T/ e9 F7 Y5 ?+ K" x+ e(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。
5 a1 ?1 E2 n3 w2 }) ~$ u' hmsado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned $ ^9 \8 z2 w* c
$ e! E7 k6 v5 C8 b8 S
【3】创建Connection对象并连接数据库 % m8 q( \( G2 z* e2 _0 U( o- P
首先我们需要添加一个指向Connection对象的指针: 9 o8 ^1 @+ y0 }' E1 X
_ConnectionPtr m_pConnection;
; q3 w6 P! b* z0 B下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。 4 @- }- e; H0 f6 V- s+ ?
1 e6 x, f- L( g) [) W

5 j+ [, W% v) `% R# Z0 ZBOOL CADOTest1Dlg::OnInitDialog()
; r! q; v, L4 |# x: q) _; i& P! |* P  { 5 F& ^% n; ^  [  V; G
  CDialog::OnInitDialog(); : b2 b1 l4 [6 _+ s; }
  HRESULT hr;
% Z+ B' s: ~4 n  try 5 U6 g" ~' T# S: t3 W3 {  n+ @
  { / ^1 M' a, _; y' S6 d3 [7 W
  hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象
2 P* \' l/ S! C! T  if(SUCCEEDED(hr))
* O& f5 r% l1 w2 ~( c$ P3 }  l: A  {
, L+ ]* _1 w0 o4 ]4 ]4 }& n  hr = m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库 5 d' B- Y5 A: a- e
  ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为rovider=Microsoft.Jet.OLEDB.3.51;  }
/ j6 Y6 ^! v1 x  } % d* |, M: b5 s/ o8 T9 D" `& u
  catch(_com_error e)///捕捉异常
7 v1 M  Y% C0 ?4 Y" o  { & n/ ?5 h) v# m  X9 x
  CString errormessage; - o1 O) {, n0 ~# @" J7 _  s" y
  errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage());
2 c% v. Y( M( u3 B/ ?+ q2 F  AfxMessageBox(errormessage);///显示错误信息
4 I6 R8 D5 U+ w, @8 p$ v  }
( a+ C1 A' H+ r7 @) ?- R$ f2 `2 M3 Q* X, ]+ C
在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型
8 _! Q- G- i5 U9 Y  n3 F( jHRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
2 ^- U% |. v" U& X4 ]ConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权, ' p$ a9 s9 {8 K/ i4 x0 ]
Options可以是如下几个常量:
( z! |: _5 d: d) eadModeUnknown:缺省。当前的许可权未设置
$ ]) v' [, L  h$ M5 ^1 n. x  JadModeRead:只读
* t. l1 q; m2 o$ H0 B# Y( X% ZadModeWrite:只写 . k4 ~. R# e; ?% r
adModeReadWrite:可以读写
( C6 k8 N" O& W- P( {0 ?: _$ U+ m0 fadModeShareDenyRead:阻止其它Connection对象以读权限打开连接
& ?- y' p& D; ~3 G( {! j% n- zadModeShareDenyWrite:阻止其它Connection对象以写权限打开连接
, _4 H) J! s; {! n% Z6 A& w: GadModeShareExclusive:阻止其它Connection对象打开连接
5 Q) \2 X  O/ i$ X  P) g5 |) d2 ]8 D, WadModeShareDenyNone:允许其它程序或对象以任何权限建立连接
% {8 q7 ?$ x; \/ E% L) @# W
+ A5 ?/ p5 a& g+ W+ p! x我们给出一些常用的连接方式供大家参考:
+ A! F" {2 V1 }0 c* w' D(1)通过JET数据库引擎对ACCESS2000数据库的连接 , M+ j; A  |- }* b$ b) s4 P$ C
$ o& M+ V0 o) ~: V( _
m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown); ! J6 q2 p1 I1 p0 U3 U* Z6 E
3 B' @" ^4 j- R6 z  X8 [
(2)通过DSN数据源对任何支持ODBC的数据库进行连接:
, W4 P1 N0 Y/ S5 q( F8 lm_pConnection->Open("Data Source=adotest;UID=saWD=;","","",adModeUnknown);
* |: ?8 h9 k0 G7 d3 V) b) G, R& f$ G; q. h
(3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown); ) ~, d8 P8 E' ]+ h

+ c/ E" L/ {7 L1 ~+ G' I: q. o其中Server是SQL服务器的名称,DATABASE是库的名称 8 K7 Y# i; }2 i- f: A

7 R' ?' w: X. kConnection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State
, a2 {1 o0 l% U/ cConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒 , B5 B$ D# L7 q$ y9 J
m_pConnection->Open("Data Source=adotest;","","",adModeUnknown); % {! g4 T3 I+ u$ Z$ @0 f- z2 d

: A9 R& @+ h% t( q1 ]9 r/ s) H# s6 S0 o1 O: s- R
State属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如: % G9 [: _/ X0 W! h' Y
if(m_pConnection->State)
3 a0 v4 d* K" e) V0 W     m_pConnection->Close(); ///如果已经打开了连接则关闭它 ( d. b2 I7 [3 h' i8 `
( z0 v$ A+ N2 X* k5 ~/ q
1 C' v/ C* ]  R; w% Y$ j
【4】执行SQL命令并取得结果记录集 7 B, _; D6 I5 D7 K( {: p
为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset; 5 E2 [% h1 V7 \5 ?
并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset");
  Z) N: b2 I+ c1 t( D$ _7 vSQL命令的执行可以采用多种形式,下面我们一进行阐述。
; I' \' k7 K3 ^- d* m  M: l* D" z
6 N( `$ J2 N' F: _, U2 r$ A(1)利用Connection对象的Execute方法执行SQL命令 $ R4 t5 x9 ]( D) W) h3 r8 a
Execute方法的原型如下所示:
) P& g% }7 Y8 w. C8 M: p_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一: * S/ P+ o' [/ Y) E$ P! V' K5 x- S0 Q
adCmdText:表明CommandText是文本命令   N: d3 u+ q* ?
adCmdTable:表明CommandText是一个表名 7 H/ |. `1 D" k: G. i8 w5 u
adCmdProc:表明CommandText是一个存储过程 ; V) r7 q4 O- k
adCmdUnknown:未知
2 Z5 V2 J) a8 a& [% g8 L& e) R/ c6 P& Y  {8 `! D0 |3 E6 f& U
Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。   _variant_t RecordsAffected;
- H" Y2 M, A7 o' i$ F3 @6 X# u9 |6 @  ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday 7 j8 k6 S& R2 t. f( ~
  m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText); ; p4 F! p( u3 U% k5 ^  l' \( O/ e
  ///往表格里面添加记录 7 y- Z2 i" L  ?* C
  m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) valueS (1, nullnullnullnullnullnullnullnullWashingtonnullnullnullnullnullnullnullnull,25,nullnullnullnullnullnullnullnull1970/1/1nullnullnullnullnullnullnullnull)",&RecordsAffected,adCmdText);
# H0 v4 g6 @. s: H  ///将所有记录old字段的值加一 ; f* c. W2 a* J% x/ o" E
  m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText); - w" i6 f8 ~; H% `! k1 y% }& T
  ///执行SQL统计命令得到包含记录条数的记录集
0 y0 r& K+ z4 o  m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);
: S, f; d$ t5 b! ^" r0 D" Q  _variant_t vIndex = (long)0;
5 v: I1 A) j2 ^* A7 l+ n- `5 F  _variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量 $ _- g1 j  t3 x4 s' P" X0 Y& K5 b
  m_pRecordset->Close();///关闭记录集
4 G5 T8 I. B0 S! l2 C  CString message;
( ]; m5 ]( d. |- q  message.Format("共有%d条记录",vCount.lVal);
$ f: {( l' b' v1 L+ {) X0 i# B0 F3 P  AfxMessageBox(message);///显示当前记录条数 ) P4 Z" f. X7 g2 c

* Q; s$ u9 f$ V
, z+ K( y: V; p2 O(2)利用Command对象来执行SQL命令
& y5 U) f# i- M7 t8 Q  _CommandPtr m_pCommand;
, w% G4 p3 w6 U. H  m_pCommand.CreateInstance("ADODB.Command"); 9 i; ?$ N% c5 f/ N+ v8 T, a
  _variant_t vNULL; 2 p: t/ o# ?8 E6 R
  vNULL.vt = VT_ERROR;
6 Y6 T8 E5 [8 g9 ]6 T  vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数 6 ^; S% q6 @- ~3 \6 m
  m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它
/ g/ d5 S7 U* l2 b  m_pCommand->CommandText = "SELECT * FROM users";///命令字串 2 B1 N9 R/ Z  k/ b- v7 p' Q
  m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集 0 D& |2 Z7 Y1 Y

6 ], _: Y% z3 Z! Q7 r: f在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。 9 e2 Y+ ?9 ^+ V* e  `- ?* v( D5 A
- I1 N# @5 `' C6 V+ w7 d- K
; b8 x0 c! {& _3 b3 s; Z0 b
(3)直接用Recordset对象进行查询取得记录集
1 p4 k6 S9 T- [" ^$ O1 r8 l  V例如
: m# [* G" g8 z" j1 N8 o
, s- o$ }8 Q! S( e2 h  m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText); $ b/ ]0 d, i1 F

/ r( W5 O* W: YOpen方法的原型是这样的: - C8 D5 D- x$ f' W% T! W0 z
HRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options )
0 m3 p% A9 N6 C其中:
- ^# T2 E: c2 i/ V; \/ ~①Source是数据查询字符串
! {! R1 @0 f# x% W②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象) # v% D: G6 m& X5 a
③CursorType光标类型,它可以是以下值之一,请看这个枚举结构:
/ |7 D. D% \# ?5 W) ]( tenum CursorTypeEnum
% i# q. W8 ~  V+ Y{ ! d' P; O7 g, J' T& b
adOpenUnspecified = -1,///不作特别指定
9 f/ J. L" z: ]: x) zadOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用
0 _/ x8 R& P1 T' aadOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。
4 |" J2 }. W: Q- madOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。 3 q/ r7 `9 L) J0 X! @0 q
adOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。
6 {5 _7 F5 }. h}; ! C8 r4 l( M% ?* p7 v
④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
- p8 Y1 Q( c4 c% A* J5 P6 @enum LockTypeEnum 1 Z% L5 |4 n: F2 w& x' a. @9 X6 a4 }
{
! C. [4 s! H% b2 JadLockUnspecified = -1,///未指定
) m5 b- Z$ H  l4 fadLockReadOnly = 1,///只读记录集
5 }9 {. }/ z7 ~/ N4 madLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制 8 \, f# A8 \- h" K7 i. n
adLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作 " P( q2 v4 g1 o2 N( x5 M+ a' R9 c
adLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。
# c5 y# {  o' H/ g, T};
1 h5 m* H. j4 t7 Q) ^6 x⑤Options请参考本文中对Connection对象的Execute方法的介绍 5 o' H/ W1 j8 C: O2 Q: P( ^6 T  i. h

) c! S. T' n8 i6 U; \3 t
+ d( ~' f& M$ s& i【5】记录集的遍历、更新 , y$ u# _% `- L) o
根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday
5 q4 ^$ A/ E; H* j) G7 X以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。   e& N: _0 P5 ?- @2 ]

1 \1 P) {+ ~  l+ y' Y8 ]5 E* D6 ^4 K( i  F; @1 D* O7 d
_variant_t vUsername,vBirthday,vID,vOld; 7 Y# [" k+ @! h8 w, Z4 q7 y% J2 W
_RecordsetPtr m_pRecordset; * x+ R: c5 H2 k5 ]7 v" Y
m_pRecordset.CreateInstance("ADODB.Recordset"); ' `: e% c* x* S/ f; U8 i" a+ t
m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText); # B0 ?' h: V7 R% g
while(!m_pRecordset->adoEOF)///这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗?
( w" V( B& N- o{ . L( R# Q& r4 }" @! x
vID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行 6 K: W) P; p% i2 A2 e
vUsername = m_pRecordset->GetCollect("username");///取得username字段的值 3 \* C! B! w; {# I* N
vOld = m_pRecordset->GetCollect("old"); , |, O: r7 a% B" w! l. T
vBirthday = m_pRecordset->GetCollect("birthday"); + d4 d9 y$ ?. Q+ E: m
///在DEBUG方式下的OUTPUT窗口输出记录集中的记录
" w( d  j; K9 }5 \  Vif(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL)
0 u0 j) j1 C& {2 i. P5 Z  TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday); 7 P% X  I5 {* L( x5 {: Q: T
m_pRecordset->MoveNext();///移到下一条记录 " M6 l5 }! \# `# Y! y
} 1 i6 \( P0 C6 k" p* _
m_pRecordset->MoveFirst();///移到首条记录 $ J9 r$ Z+ J. ]: u' U: _
m_pRecordset->Delete(adAffectCurrent);///删除当前记录 ' ?$ N% s! K% R4 i
///添加三条新记录并赋值
* o/ a. I$ @+ L; \" r% D. t/ Mfor(int i=0;i<3;i++) ; F. }# d' H4 Y) S, u* r+ T
{
, w* _* h& M( u% `1 G/ e3 l# |5 `7 ym_pRecordset->AddNew();///添加新记录 1 A$ ^4 ?* o. _: U, D, f
m_pRecordset->PutCollect("ID",_variant_t((long)(i+10))); : |6 Z) O# X2 _3 h% k% c
m_pRecordset->PutCollect("username",_variant_t("叶利钦"));
* ~4 `8 \6 s$ O' Y5 cm_pRecordset->PutCollect("old",_variant_t((long)71));
( [+ W* D) M/ v0 z6 _7 A( Cm_pRecordset->PutCollect("birthday",_variant_t("1930-3-15"));
, D7 _0 H2 q& l+ O8 C5 W, N8 z} # S1 t8 t; g+ x
m_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处 % h  c7 b* [9 ]. Y6 M' j
m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄
3 L* B- d+ J9 M  ^, k( s" H) c3 bm_pRecordset->Update();///保存到库中
+ M; B! `' M6 m* x9 `; Z% u1 p% s8 D1 |' G: W; y
【6】关闭记录集与连接
7 [- A, ]( [* H$ o1 v记录集或连接都可以用Close方法来关闭 - K/ H* r: r4 O% y
     m_pRecordset->Close();///关闭记录集
/ j7 h' l0 j2 ?      m_pConnection->Close();///关闭连接 ( {9 P: Y& o0 N/ ~. E
6 n# C% U/ c2 {. I( G' z
至此,我想您已经熟悉了ADO操作数据库的大致流程,也许您已经胸有成竹,也许您还有点胡涂,不要紧!建议你尝试写几个例子,这样会更好地熟悉ADO,最后我给大家写了一个小例子,例子中读出所有记录放到列表控件中、并可以添加、删除、修改记录。
& X  Q+ z7 A$ N# j点这里下载示例代码
6 N' A; I' C2 i
/ G- S; ]2 L6 y; D# h5 H后记:限于篇幅ADO中的许多内容还没有介绍,下次我们将详细介绍Recordset对象的属性、方法并解决几个关键的技术:绑定方式处理记录集数据、存储过程的调用、事务处理、图象在数据库中的保存与读取、与表格控件的配合使用等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2026-6-18 10:12 , Processed in 0.018893 second(s), 15 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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