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

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

[复制链接]
发表于 2005-9-1 23:30:26 | 显示全部楼层 |阅读模式
  一、ADO简介 6 o" F* X& c! k
ADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。
1 G! p# w5 @0 e# Y, n0 p本文示例代码 % w8 {2 C8 X) e2 j5 N) t5 P. U
$ w2 m* Q! }+ Z1 _' \0 H' {1 \
二、基本流程 1 E. G6 m, d7 k
万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧!
9 P# C* D% ^1 `" K/ ]/ Q6 X! o(1)初始化COM库,引入ADO库定义文件
$ o9 V& q6 Z% u1 e8 S9 z: B  ]( G& w2 T. N(2)用Connection对象连接数据库
8 k7 p$ r0 ?7 r" v5 [(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。
3 n: e0 b0 i& M4 E; e* c! L) e5 G(4)使用完毕后关闭连接释放对象。
5 Z* S  m; c# C1 G( f) e: M: Z2 @) `3 m) f4 D
准备工作: 4 S5 X3 ^! T" `  W, g
为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。 6 m/ X5 Q6 B: [* C6 M/ m
下面我们将详细介绍上述步骤并给出相关代码。
) w! W5 R, O; ]& j0 Q- `3 s' w7 v【1】COM库的初始化
% G, Z8 G. A/ y# s3 Q我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码: 5 ?  r* U9 F  j& n# ?7 u7 X' y0 N

; G2 ], R& ]1 n, x# Z, j, p: m
3 c- g: {- M$ j$ A. _BOOL CADOTest1App::InitInstance() 5 W% W* y* T6 O! i
  { 6 ]) j: [& F/ u% G4 C0 p! R8 K
  AfxOleInit(); $ h8 \. v! q" S
  ...... - G+ U) s) V' K$ f- l4 H
" k8 I9 s. K3 f& D  D) {
【2】用#import指令引入ADO类型库
0 \5 Z! B% B+ a1 A, S我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到) , H1 W. u* ^5 K
#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF") 2 x3 F# I7 b# u3 D) b
这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。
  z- d5 [- s+ b/ K3 u; M& s  G6 L: I# {8 L0 I! _4 u+ [
几点说明:
2 R' r! f+ \3 U3 }9 S* t4 E6 W4 s  j(1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改 4 X% x- ^' ^/ x/ h! F; Z
(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。
3 ^/ p6 q2 e( }: ?5 g. j( \. s" Y7 bmsado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned
- N, n" @: Q2 d. g; s2 v9 m) z- b; ^2 T% K5 a% Z/ o
【3】创建Connection对象并连接数据库 1 i( D  F' J* _2 f( ^
首先我们需要添加一个指向Connection对象的指针:
! H5 g0 K# o7 Z_ConnectionPtr m_pConnection; 3 p2 W$ {/ l: {- p0 G$ U
下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。 7 S, s# X% f7 ]: G! m5 l

* K8 Q. w' b: i/ A% g' G/ n- }" G$ w$ l% |, {; U) m& r: p+ l
BOOL CADOTest1Dlg::OnInitDialog()
/ C  I4 X! Y5 S- p; v0 u4 ^" M  { & Q2 b# ~" O: Y7 G; H
  CDialog::OnInitDialog();
$ l/ }! c# F8 B. a4 U* a  HRESULT hr;
' }. ~0 V' ~( l1 H$ v  try 8 ?7 P" _5 P' `* w: f6 K: }
  {
5 `% C& O) i4 B) `! W" a3 P" z  hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象
( v* j4 T, h) S) @" U5 Y  if(SUCCEEDED(hr))
# Q. ]" ~6 y2 M( k, j  { + Q6 Z4 ^: n3 ]( S' z
  hr = m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库 % ], y$ {- V. J- n. C* C- E
  ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为rovider=Microsoft.Jet.OLEDB.3.51;  } , c: O2 @; R; q1 r; b
  }
! F# y& x! {6 e  catch(_com_error e)///捕捉异常
4 x& h7 X7 R8 _  { 5 Q, A. `. k2 I
  CString errormessage;
0 G& m9 r  k& P- d  errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage());
7 W: [% {  W1 ?$ a  AfxMessageBox(errormessage);///显示错误信息 $ V  ^& A" x' e  f+ t# z
  } 0 [  y* K5 Z* l+ T9 n* m. m% j
' C7 C8 [  ^) x6 N8 N2 z
在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型 5 f- S7 e5 d' A2 e0 Z' ?
HRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
( q5 R) G5 T8 L5 ~* n7 kConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权,
2 A8 d1 E7 A* h+ R' JOptions可以是如下几个常量: 1 x5 v+ l; r6 I8 h7 S2 H( h# p* t
adModeUnknown:缺省。当前的许可权未设置 & e6 Q$ p! N8 H8 H9 g
adModeRead:只读
* Q/ N% J+ K& ?( O: n1 FadModeWrite:只写
/ V% W3 |4 u  z) RadModeReadWrite:可以读写
# L6 M8 h; A, ]) x; z# H- K7 n, H$ HadModeShareDenyRead:阻止其它Connection对象以读权限打开连接 0 G2 s( H3 E0 F) ?* n! e1 [
adModeShareDenyWrite:阻止其它Connection对象以写权限打开连接 2 R% L; q& R/ m
adModeShareExclusive:阻止其它Connection对象打开连接
& ]# m8 z1 p8 ?) L4 p3 x, y% N, CadModeShareDenyNone:允许其它程序或对象以任何权限建立连接
0 {% t7 f* n2 }3 k6 m  j3 y. W- y, g& E' z7 n8 }" t/ W
我们给出一些常用的连接方式供大家参考:
# W$ `; h  A5 i+ r. F4 a$ B(1)通过JET数据库引擎对ACCESS2000数据库的连接 & e  V0 o8 ^  i

" e( Z! w( s. O8 c! Vm_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown);
) I8 W. ]* \) a9 c* H5 w. A% o& m* j3 G% B4 h4 m5 [
(2)通过DSN数据源对任何支持ODBC的数据库进行连接: 2 A% a! `0 l$ w0 J( T
m_pConnection->Open("Data Source=adotest;UID=saWD=;","","",adModeUnknown);
2 b' o0 W" y5 W" E6 ]3 A, H8 O
' P' `; c: S* [- n; `( P# Y(3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown); * n0 F+ U- f% i8 S2 j
: `* \3 C: p4 X1 R& u9 b4 `3 Q' c
其中Server是SQL服务器的名称,DATABASE是库的名称 2 z: v* s. n5 T" \- `% x5 Z

6 x' f" h9 H" ]% FConnection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State
( W8 P" L6 m2 W9 m3 o6 r3 BConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒
6 v$ Y1 s4 r1 r1 ym_pConnection->Open("Data Source=adotest;","","",adModeUnknown); % y* P% g% i+ s9 @* U" y

1 F: ^0 V4 ]% D- z9 ]( ]
- B: ?* e% M- ^, hState属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如:
4 y( Q5 m+ P1 Fif(m_pConnection->State) 9 z: b  V0 L, O6 n5 [3 j/ e0 D
    m_pConnection->Close(); ///如果已经打开了连接则关闭它 8 K; o' w2 ~$ H" i* H
; i& v7 L% m6 x" K3 `
1 ]- J/ J' \* x9 h% V
【4】执行SQL命令并取得结果记录集
4 D: ^7 ]' w7 h; s" E为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset;
+ |( U: n; |2 k0 {) @9 @并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset");
3 [: d" z) o* s# E, GSQL命令的执行可以采用多种形式,下面我们一进行阐述。 ( P4 x# ~6 Z3 ?* ]3 v. R$ I

7 Q9 n- B5 p5 O; m( U  M(1)利用Connection对象的Execute方法执行SQL命令
! C$ _7 ?0 U3 f. L% YExecute方法的原型如下所示:   j* e& {2 G  I. Q  w
_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一: * G' L4 w) p8 |
adCmdText:表明CommandText是文本命令
& @6 Y0 p8 G  a- Z1 B; G8 DadCmdTable:表明CommandText是一个表名 3 j+ W& q; Z  d8 N' u
adCmdProc:表明CommandText是一个存储过程 , b% m: V. e, [7 O  A4 ]% J
adCmdUnknown:未知 ( |  p  ^7 G! q, @; y
3 V7 {+ n2 s6 ^3 B% N' m; W. u
Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。   _variant_t RecordsAffected; % ?" J+ q! ]0 ?
  ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday
, s, N- I7 `* y) d) d  m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText); 4 \; o1 p4 `" \/ H
  ///往表格里面添加记录
4 Z& O5 H! {% x0 n  m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) valueS (1, nullnullnullnullnullnullnullnullWashingtonnullnullnullnullnullnullnullnull,25,nullnullnullnullnullnullnullnull1970/1/1nullnullnullnullnullnullnullnull)",&RecordsAffected,adCmdText);
7 O; [9 |+ ~, K  ?  ///将所有记录old字段的值加一 4 M! x: F# v) h
  m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText); . s( i9 r2 l  M3 X6 D
  ///执行SQL统计命令得到包含记录条数的记录集
: w5 N, j1 N: Y$ I6 b1 R; Y  m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText); ' Z$ ?9 @% }/ {' l7 {( C+ u! h
  _variant_t vIndex = (long)0; 6 f$ D) i- \% \/ d$ X
  _variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量 / P2 m. }5 A* @& P/ R
  m_pRecordset->Close();///关闭记录集
) c, y: S4 }& J/ o7 p& ?  CString message;
" J4 h# b( h1 R  message.Format("共有%d条记录",vCount.lVal); ; q) Q# o+ t; o! O3 H- O+ I( }
  AfxMessageBox(message);///显示当前记录条数
5 ^0 r2 P; x, j7 U. J9 q
4 q$ z4 l* p0 ^* i3 J: C5 m! ~/ B( a% @: P
(2)利用Command对象来执行SQL命令
# B' Y( l7 W( \" i  _CommandPtr m_pCommand;
$ `, o; w9 {( X2 b5 @; G  m_pCommand.CreateInstance("ADODB.Command");
" {, X* e% \6 ~5 p* U7 ?  _variant_t vNULL;
7 k3 K& r. O0 g$ ]( S0 F8 i  vNULL.vt = VT_ERROR; ' @# r7 `* ~( X# t8 Z4 u
  vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数
; ]9 w0 o' |$ z8 x% F# k& l2 n6 @  m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它
5 A! f: d" d* K; x% a6 F  m_pCommand->CommandText = "SELECT * FROM users";///命令字串
0 D. l  m2 h. i. V& a% P* r$ g  m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集
- j" R/ m" l1 q$ H9 \) C
$ N/ S; C+ @$ q0 z* Q在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。 5 h& k; d! M$ b+ j, K6 ]% J  V4 P

( a- D$ T, u- O% `( h$ [* f4 p/ b0 ?5 }: w" p5 x3 B
(3)直接用Recordset对象进行查询取得记录集
: m2 u7 b+ W0 E  O1 K4 |' n例如 ) X2 Q, V1 K, [
; N- f! M. i1 H
  m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
5 k0 b7 z8 d( _
! X' v8 z( P6 i7 q( h, D2 K! i) COpen方法的原型是这样的: 0 d; Z+ Y0 g+ X7 W5 z) p
HRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options ) 6 I$ F7 ~2 v+ N* Y7 i
其中:
! N0 K$ @+ m  B  S$ y+ _1 |①Source是数据查询字符串
& u. P) \; w9 o②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象)
# W% ?) V% `; [: t3 w2 \③CursorType光标类型,它可以是以下值之一,请看这个枚举结构: * l. i& q3 j8 d, J1 m' G% U2 ~1 q
enum CursorTypeEnum + S$ k( A! J6 J! s# M5 `7 m
{
, c' A- W/ R$ h! b  y( j  E/ EadOpenUnspecified = -1,///不作特别指定
# e0 E0 E- U4 c% d8 V9 U3 j/ j9 {; S3 e! |adOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用 / [0 z1 J/ D+ p' X: _  m" z* i
adOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。 1 R3 M7 }' t/ z) p
adOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。
$ G2 i. @  k3 i5 D1 DadOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。
3 Y5 x7 Q7 Q# z6 E8 ~}; 3 {) P7 S5 M% g7 k2 i
④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
" b# b; a: g, Z! d1 n% p: ~enum LockTypeEnum 4 g' ]7 N2 u% F
{ 9 w# W$ e3 R4 X7 C7 D
adLockUnspecified = -1,///未指定
7 C% G6 L6 m5 d5 padLockReadOnly = 1,///只读记录集 $ c- W) X0 H% S  y8 W
adLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制
) V+ g8 a! }' VadLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作
4 y* V* W/ f" @- t2 B# c: u; R* nadLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。
+ m/ q# a6 I' R  J};
  S9 r$ {) J8 P( {* `8 t⑤Options请参考本文中对Connection对象的Execute方法的介绍
) n2 T2 d9 e4 L' P
' ^; k8 n9 F7 u4 f  X% ?& x1 I$ t1 p6 |6 x/ [" X" c
【5】记录集的遍历、更新
- c' U4 G. O) k7 w( q' k根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday " Q- X  M& @+ q) y
以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。
# S4 b, D' a& l# N, B
; i" p6 ]0 E2 @" p2 o
  W$ C4 v1 p. L+ [_variant_t vUsername,vBirthday,vID,vOld;
$ d2 O' L, }4 x+ P8 `_RecordsetPtr m_pRecordset; ' s+ ~- v4 M7 j2 K& j
m_pRecordset.CreateInstance("ADODB.Recordset");
. ^% C( Q7 B+ A! `% P) nm_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText); 2 C' P- q7 r& U( U
while(!m_pRecordset->adoEOF)///这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗?
& S  M8 c, S2 o. h% b+ S{
* z# {' I6 c1 _5 cvID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行 * D4 \+ v6 g4 r$ B, m2 ~
vUsername = m_pRecordset->GetCollect("username");///取得username字段的值
1 V2 F) _; ~* M8 f: L4 Z0 W) n+ MvOld = m_pRecordset->GetCollect("old");
4 ^* w/ I8 a) EvBirthday = m_pRecordset->GetCollect("birthday");
0 |" ~- Y7 V& t  U- ~# g) V///在DEBUG方式下的OUTPUT窗口输出记录集中的记录
" x9 d& k, e( M" Zif(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL) . b# e0 p$ K' W3 s/ v7 W
  TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday); 9 h: ]  F% E4 E: }; Y" ?# c5 j' }
m_pRecordset->MoveNext();///移到下一条记录
* ~7 j) ]1 j2 y5 C7 I6 J}
. i+ Y2 q8 z9 [  j  U0 M( wm_pRecordset->MoveFirst();///移到首条记录 7 N  |, H3 x  D: y+ J  Q4 H
m_pRecordset->Delete(adAffectCurrent);///删除当前记录 + P4 K9 E9 Q- }) m/ m
///添加三条新记录并赋值
" K. u) e  k+ J& k$ O$ Ufor(int i=0;i<3;i++)
2 Q) x- ]2 f8 d# j: s{ 7 K( ^$ A$ M. L/ P/ I7 ^6 R
m_pRecordset->AddNew();///添加新记录
/ `8 B' {0 L* O# E0 _m_pRecordset->PutCollect("ID",_variant_t((long)(i+10)));
: C0 K5 r6 q/ ?* Z; Em_pRecordset->PutCollect("username",_variant_t("叶利钦"));
3 o1 {: b( ^, H6 am_pRecordset->PutCollect("old",_variant_t((long)71)); ! ^  A" o- B% j4 B6 E6 j& d
m_pRecordset->PutCollect("birthday",_variant_t("1930-3-15")); 9 }) `1 E5 z9 q$ v, g( i1 }. o0 o2 B# u
}
; b  i* `1 S% q, Tm_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处 * z) a0 {) m7 Q0 m1 w
m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄
3 {) v+ H/ G) X' `, {5 xm_pRecordset->Update();///保存到库中
9 v) L6 N( b$ d9 z4 }8 ~$ y
8 k7 `( p) P5 b* z$ S8 K【6】关闭记录集与连接
- W7 q; d) k$ o. r  X1 Z6 P8 R记录集或连接都可以用Close方法来关闭 6 _, D, C& L1 r" ^/ \% Y
     m_pRecordset->Close();///关闭记录集   R/ V" }7 n& I
     m_pConnection->Close();///关闭连接 : w6 I/ |; _0 [9 `

$ N7 [" Y8 D% L3 l至此,我想您已经熟悉了ADO操作数据库的大致流程,也许您已经胸有成竹,也许您还有点胡涂,不要紧!建议你尝试写几个例子,这样会更好地熟悉ADO,最后我给大家写了一个小例子,例子中读出所有记录放到列表控件中、并可以添加、删除、修改记录。
2 a9 |$ O- ]9 J# T" [. u9 e7 S4 y点这里下载示例代码 / ]; w2 @/ H0 T2 ~3 R0 f1 R( J! b+ M
/ @/ E' T7 y0 R& C8 c
后记:限于篇幅ADO中的许多内容还没有介绍,下次我们将详细介绍Recordset对象的属性、方法并解决几个关键的技术:绑定方式处理记录集数据、存储过程的调用、事务处理、图象在数据库中的保存与读取、与表格控件的配合使用等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2026-5-2 10:34 , Processed in 0.021018 second(s), 15 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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