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

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

[复制链接]
发表于 2005-9-1 23:30:26 | 显示全部楼层 |阅读模式
  一、ADO简介 ( O( o& X: M: l) m+ F) [
ADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。
+ E8 ]" Z0 ?; I# r本文示例代码
/ E, \" W9 A! O! P, D- R
2 M; o2 Q; K7 R  y/ r3 ~二、基本流程
( g/ L. |' H' A, I. [9 k万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧! 3 _$ b4 n- W1 Y& ?  L
(1)初始化COM库,引入ADO库定义文件
/ \  P8 f, w2 [% B(2)用Connection对象连接数据库 * z- C- f9 j0 U- C- u% ?) c" m
(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。 5 a5 m9 D( |( i' c% X- E2 g" @
(4)使用完毕后关闭连接释放对象。 9 [3 ~( s, |/ x' c2 m# Y
! j; |/ C  n  P3 w1 F) `
准备工作:
6 i4 k: s! @# U5 e( h3 ~7 x为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。
2 _$ n4 C+ n/ c下面我们将详细介绍上述步骤并给出相关代码。 5 s# D4 }8 y! k" |
【1】COM库的初始化
( a2 `& e  d0 v8 f' U我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码:
- O$ i: ~- P$ a9 |0 y2 U: |$ B* m4 o9 q- V" T$ y7 _# X( ?
3 _& r1 C, p, \& }; z# D$ M1 u$ C
BOOL CADOTest1App::InitInstance()
3 X% Z2 F7 m8 H* m" \4 c  {
. k6 |" h3 d# I  AfxOleInit(); & c! X& v, M+ b# ^5 e/ ?
  ...... ) v3 e7 Z0 N9 s  j& f2 M$ b

/ l' a0 ^  x1 O9 H+ o【2】用#import指令引入ADO类型库
2 A& S% q3 V( t) t9 A1 h我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到) ; Z- h) N7 F( O( l
#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF")   Y- T( ^" }# f: x3 q- D
这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。 , o/ M9 d6 k" G- G
2 ^# h8 m; q4 L1 C# M5 o
几点说明: 6 f! F+ a, F! @0 ]- C- c9 @. N
(1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改 ( l5 b8 |' `0 J1 X1 s
(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。 2 }5 z2 N) r' S) V9 @! A
msado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned
2 g" x+ G# V; Z$ y% y2 W( B. b+ [$ w/ j4 D9 A$ B* n8 |  r1 v
【3】创建Connection对象并连接数据库
% y/ k: R/ `0 ^( Q首先我们需要添加一个指向Connection对象的指针: 3 N4 o: I- u9 V" }% @% D: k0 ]8 _
_ConnectionPtr m_pConnection; : A. |' c3 a1 Y! I4 K
下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。 6 B& x/ W* S: e: M2 D9 T! D5 }  {
" i) m# s2 m; ]' ^2 s0 ]5 E- H
4 b7 T7 |/ K0 o0 H7 o2 t
BOOL CADOTest1Dlg::OnInitDialog()
& N1 I' B# X7 Q. D4 u$ Y! }+ u, X  { 3 i4 y3 _/ _; V* L3 \% T: H
  CDialog::OnInitDialog(); & v. |' C( O' a; ^9 v. f
  HRESULT hr; ; D  u& L$ B" k. N  [* P. ]. d
  try 3 i1 ^4 y' X" ]% q
  {
1 q% U8 p+ J) q, m  hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象 / Q& m( N3 u& Q. Y8 ^/ ]6 z; @
  if(SUCCEEDED(hr))   M  w4 f9 [- U3 W2 x( C, i% b! J
  { $ K6 q/ H9 w( I7 L9 R
  hr = m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库 * W* W/ Z3 S- a  a( k8 D
  ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为rovider=Microsoft.Jet.OLEDB.3.51;  }
5 j( {4 J% z7 Q  }
% s: E6 E" R9 J( w  catch(_com_error e)///捕捉异常 : z7 a5 m1 q. V( \+ a! D/ m
  { 5 I- u( k$ g1 f) ?
  CString errormessage;
4 ~1 Q" F6 N1 u  errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage()); 6 [0 x+ b$ y( r+ [' x: u- t
  AfxMessageBox(errormessage);///显示错误信息 2 z9 I) C% g5 m! A( D0 Y( [3 m
  } ' [$ `* B4 y6 Z' H# s2 I! p

7 Z, D: u8 [# i7 l在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型 6 Z$ |: L' R4 K/ f5 i% H
HRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
( Y& b6 b0 k0 U/ v1 k* tConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权, 0 V; S: R' G; f  G
Options可以是如下几个常量: " W  P0 j/ F* F1 Y2 z
adModeUnknown:缺省。当前的许可权未设置 ! T( K! m/ `& o' G
adModeRead:只读
! c( Z4 q6 O+ ~1 hadModeWrite:只写 7 W  H: }$ M- L2 @  V" N# k$ _
adModeReadWrite:可以读写 6 d  d4 Z) q# J% i3 L
adModeShareDenyRead:阻止其它Connection对象以读权限打开连接 5 G- y- z# h( f: S  m( k6 p! p+ h
adModeShareDenyWrite:阻止其它Connection对象以写权限打开连接
  e- ]! t3 e8 h- _! {! o& ?+ r# R4 [adModeShareExclusive:阻止其它Connection对象打开连接 % q: E' M1 p1 [- c: u& v
adModeShareDenyNone:允许其它程序或对象以任何权限建立连接
' O2 t* u: {8 K9 J
) e  Y: H6 e9 Q2 B/ g( \3 G$ `我们给出一些常用的连接方式供大家参考: ) d% Y8 b7 ?8 _0 y% c/ A, u7 h
(1)通过JET数据库引擎对ACCESS2000数据库的连接
' N3 r) x& d( p8 i3 |2 [9 l
3 U8 B$ b0 Z9 c2 Sm_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown); 4 y7 L9 [* j( B
( C; B' _% L& }2 |2 B' Z
(2)通过DSN数据源对任何支持ODBC的数据库进行连接: ) f7 w' W8 d9 D6 \5 A$ g, u
m_pConnection->Open("Data Source=adotest;UID=saWD=;","","",adModeUnknown); ! @7 ?. V' ?# R: ~
% Y% Z2 v* f* `
(3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown);
- ]8 V9 R1 X0 Y3 t) n7 I, S7 L% z$ F/ O& _$ h8 ?1 D8 O
其中Server是SQL服务器的名称,DATABASE是库的名称
. M6 {; M9 J; W+ D! @' Y( D; F5 N+ K5 p
Connection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State & a, c( v% i9 Z
ConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒 , L' t3 y, c! [, ^0 ~
m_pConnection->Open("Data Source=adotest;","","",adModeUnknown); % J) E" @, x0 C% D1 X

, M% t* b& j' j1 g. f
. |6 {$ d! e- K- P# Z8 VState属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如:
7 V# {. L. y9 s  Q  `; F2 P/ r7 C* Yif(m_pConnection->State) ) m- T7 c0 G5 P- B1 ]
    m_pConnection->Close(); ///如果已经打开了连接则关闭它
! V3 h# ~. Q) j4 X5 F: Q- A5 u/ }) `( t1 B" s5 w. m( e) {- K, x! O; U
+ X# T$ K: L8 X8 g% a$ x; s
【4】执行SQL命令并取得结果记录集 3 O; b! q+ |9 X, ]
为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset;
! d# B  l4 c5 L6 x! S9 X! p9 P并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset"); & E0 _" x: f2 ^% M% T
SQL命令的执行可以采用多种形式,下面我们一进行阐述。
# d; L8 T- y& L4 t
- z( k0 {, ?1 P9 l' J) H5 b7 H! E(1)利用Connection对象的Execute方法执行SQL命令 8 F' L# K" c  N5 y) ]
Execute方法的原型如下所示:
+ q& |# w# O# h& S2 m" n# l_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一:
% G; r7 C2 R/ N# R2 EadCmdText:表明CommandText是文本命令 " j4 T+ A1 a/ ?
adCmdTable:表明CommandText是一个表名 : n/ @; z  G: J3 T  ]. C8 a
adCmdProc:表明CommandText是一个存储过程 $ `  H+ T4 \8 p5 t, W
adCmdUnknown:未知 4 ]: m* d7 j  `! \$ i5 r) ]

7 \. ~; b# l/ X" O/ |! U7 E' \Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。   _variant_t RecordsAffected; ! W! a6 y7 O- V8 }& G
  ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday : B5 q; L, L  l2 c, ~! p  }
  m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText); ! g8 L. x4 F$ T9 o$ [
  ///往表格里面添加记录 $ W4 f+ R0 M$ D8 j; x, _
  m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) valueS (1, nullnullnullnullnullnullnullnullWashingtonnullnullnullnullnullnullnullnull,25,nullnullnullnullnullnullnullnull1970/1/1nullnullnullnullnullnullnullnull)",&RecordsAffected,adCmdText);
. w! C! x! b: l$ L$ w9 v  ///将所有记录old字段的值加一 # ^! g2 T+ B. O5 U! h! x
  m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText);
4 s2 A2 ^  D; w- j' {6 M8 [1 Y  ///执行SQL统计命令得到包含记录条数的记录集
# @8 T" t- y, f9 _7 d- L( N) W  m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);
; i9 h* v1 U6 g8 V! P  _variant_t vIndex = (long)0;
% z  a" L  @5 X/ f  _variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量 % ?4 _) n- q  P7 l7 X" I
  m_pRecordset->Close();///关闭记录集
  w  ~- E% t# c. I  CString message;
8 ]: E  `- v  ~8 U' d( q  message.Format("共有%d条记录",vCount.lVal);
; u3 U9 }  z" e* Q  AfxMessageBox(message);///显示当前记录条数 : o2 d) b, q9 X/ Q

9 M' {6 [: \9 {+ M: n* @$ R1 D& f# l! ~3 v( N/ A' x
(2)利用Command对象来执行SQL命令
$ |" V3 o& X! E% w  _CommandPtr m_pCommand; 7 ?  `  P! q, @- k
  m_pCommand.CreateInstance("ADODB.Command"); / N* Y: a" s$ q2 w
  _variant_t vNULL;
1 s# d6 L# E. j% a2 A4 l  vNULL.vt = VT_ERROR; ( {/ _! ?' |! O' @2 q/ S
  vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数 & n+ u9 b) g& [( H- ^
  m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它
. [+ J5 B9 D  K( G  m_pCommand->CommandText = "SELECT * FROM users";///命令字串 % d( A( Y1 ~* H  W' V' q' v4 i
  m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集
  h+ r' B; e& q( ^) w; S- j/ n
2 s2 o# d! z3 W4 L9 Y; j0 ~在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。 2 |! b" z1 p0 Q

/ @1 p- r+ Q$ E4 R4 i- \" F! M5 P+ a) y7 q- I
(3)直接用Recordset对象进行查询取得记录集
2 N5 H4 S9 M4 g; q例如
$ T/ x0 e  |1 O3 W4 P, Y! Z: z# E. k5 A- \+ {$ f) c# D
  m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
9 v+ C( b  U4 n) W9 z2 B% p/ c6 x; U( k4 @9 S
Open方法的原型是这样的: ; k8 u4 Z/ Q" O' T; R3 P
HRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options ) $ v( V" G) M: W6 q! A1 U) }! i
其中:   q3 _0 c7 ^' k% N
①Source是数据查询字符串
' B( r( W) E6 D% x5 w②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象) 2 m& K& V% K6 J6 \- U% _
③CursorType光标类型,它可以是以下值之一,请看这个枚举结构:
1 H! U& N3 J' L3 V& yenum CursorTypeEnum
0 o0 u5 }' ?* |& b0 W! \+ A! V. J{
4 ~' A1 w2 J+ z* C  {+ h2 e! U# {+ CadOpenUnspecified = -1,///不作特别指定
/ D& H5 Z& ]& i  L2 ?: badOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用
* M  U1 o" e- u) d. RadOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。 % {( @" l. b" P
adOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。 / z& d& k% ~  u; q# K
adOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。 + O  a( I' {- M4 q8 ?
};
9 J8 b; C. k+ ?, ?) p④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
. I+ N. j5 c3 S3 l9 W( |enum LockTypeEnum & e8 ]1 Y5 m' j% G* R9 i
{ % V! g5 X5 H! \/ q0 _4 C7 y2 }
adLockUnspecified = -1,///未指定
  M% o0 ]7 J2 ^; _' ^) yadLockReadOnly = 1,///只读记录集
" Q$ W; E0 H% \2 F! t; x2 ladLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制
5 W$ m2 i+ [1 P9 _+ A' ?adLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作
% y6 q+ s$ Y& i. m; LadLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。
- m4 ?" S6 ~8 Q+ V4 L  Z}; 3 w% i8 F& s4 w7 y5 i2 S# {) x9 b5 h
⑤Options请参考本文中对Connection对象的Execute方法的介绍
/ o3 b! v2 e) w5 n( g
+ [; A" |# I, W* Z2 F) T" o3 Y7 C9 z+ ^1 S! a& {! ]) F3 ~
【5】记录集的遍历、更新 1 c8 m$ Y9 o- j! Z
根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday   u# l5 h0 L& ^' `7 l8 K
以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。
* Y" z9 h6 }, b: y% O, ?1 Q  Q
! a5 ]/ q2 u# f$ e- M5 m/ U2 v# N# t: {0 j7 ^& @; T4 n
_variant_t vUsername,vBirthday,vID,vOld; 1 A0 y* c" F. w* l
_RecordsetPtr m_pRecordset; 2 b7 [8 [) l& o* Y: `' H6 o7 V
m_pRecordset.CreateInstance("ADODB.Recordset"); ( u+ l. L/ s' K+ d3 K
m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText); 3 u6 Y5 a5 u: N1 U/ R, I
while(!m_pRecordset->adoEOF)///这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗? + G8 P* `  x1 b0 |
{ 9 J3 m7 y3 N3 x) `! e
vID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行 5 o+ S% }) c; L, @' c
vUsername = m_pRecordset->GetCollect("username");///取得username字段的值 ' u0 i% ~9 U0 t/ Z% R2 v* {1 m
vOld = m_pRecordset->GetCollect("old");   h: u/ K1 c3 B
vBirthday = m_pRecordset->GetCollect("birthday");
/ Y. z4 ]  X# e+ g9 V///在DEBUG方式下的OUTPUT窗口输出记录集中的记录
6 i( V8 L+ Z# R4 Sif(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL)
, a& Z1 w- F5 Z) G/ k  TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday); % Q6 B; m) G6 N  O2 U3 I3 Y, Q
m_pRecordset->MoveNext();///移到下一条记录
; V9 w7 a. d" D9 L, e( [% Z} ) h: v1 p1 ]9 u+ [
m_pRecordset->MoveFirst();///移到首条记录 2 r+ `" A  t7 M  ?
m_pRecordset->Delete(adAffectCurrent);///删除当前记录 5 ^4 u$ {9 K2 U0 l
///添加三条新记录并赋值
# R: O. C6 N+ e+ S3 b+ y8 [5 jfor(int i=0;i<3;i++) ! u7 r6 ^0 C4 x
{
, Z6 }' g; z4 B8 Em_pRecordset->AddNew();///添加新记录 ( h6 x7 C) w$ T; h: y  U
m_pRecordset->PutCollect("ID",_variant_t((long)(i+10))); # Q) ?3 J. @" S+ L* |, _
m_pRecordset->PutCollect("username",_variant_t("叶利钦"));
  d9 J% N9 i5 z4 z8 t3 om_pRecordset->PutCollect("old",_variant_t((long)71));
3 J- n$ X; h' |2 @6 q& O9 l) L" o) ym_pRecordset->PutCollect("birthday",_variant_t("1930-3-15"));
' S5 |, ^; v, C}
: P( V& v+ j) \3 G, C3 X  P+ dm_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处 2 ]! X; r$ @6 E! t
m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄 ( j  J$ B8 n+ \, T$ L+ S
m_pRecordset->Update();///保存到库中 ) _; q/ w( a; R& E/ O; j
8 |' Q- d( o+ @3 P
【6】关闭记录集与连接 ( ?6 ?( @* a% c/ Z8 I2 j
记录集或连接都可以用Close方法来关闭
' l# G! I9 |/ ^# e      m_pRecordset->Close();///关闭记录集 4 }7 G: c* e9 c5 c' m& D% r* y
     m_pConnection->Close();///关闭连接 % S: L" X) {) |! e0 s* z
6 }6 s5 x' u  Z( y; k3 K/ z
至此,我想您已经熟悉了ADO操作数据库的大致流程,也许您已经胸有成竹,也许您还有点胡涂,不要紧!建议你尝试写几个例子,这样会更好地熟悉ADO,最后我给大家写了一个小例子,例子中读出所有记录放到列表控件中、并可以添加、删除、修改记录。
. U5 r. d. L5 N$ V$ H点这里下载示例代码
& e* m* z/ Q% |, b7 l7 Y# V& q+ G2 M4 b6 I; u
后记:限于篇幅ADO中的许多内容还没有介绍,下次我们将详细介绍Recordset对象的属性、方法并解决几个关键的技术:绑定方式处理记录集数据、存储过程的调用、事务处理、图象在数据库中的保存与读取、与表格控件的配合使用等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2026-8-14 01:02 , Processed in 0.021357 second(s), 15 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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