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

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

[复制链接]
发表于 2005-9-1 23:30:26 | 显示全部楼层 |阅读模式
  一、ADO简介 ; ~, \8 O8 ], q, J1 v
ADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。
( H1 p% o( H3 _! ?2 H2 X+ w7 ?本文示例代码
- m4 }- K! l3 b) S) o+ c$ K! W/ u6 m- `- b; h/ z8 z9 L& s7 f
二、基本流程 3 |1 E3 v4 t4 C* D1 k
万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧!
' Y. H6 e0 g. S6 p5 d& c' T(1)初始化COM库,引入ADO库定义文件 ( `3 Y7 }2 v6 J# r
(2)用Connection对象连接数据库 / Z9 l/ }6 Q# [( T! J
(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。
( Z; g' b# X* m% ^(4)使用完毕后关闭连接释放对象。 # j) s1 r- ^! b4 }& v- ?
' o2 U/ O4 s, n6 G" {) v6 J
准备工作:
" C( ?. U. B! U9 v( W3 K; ~为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。 ! d& S2 d. t; y
下面我们将详细介绍上述步骤并给出相关代码。 + l) G4 R/ g0 u) P8 @" d
【1】COM库的初始化
& X- t( o/ D/ O: y& s8 e# Z我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码:   \# g! u$ b: E4 x5 p" ]7 q) q% B
7 [3 H4 q! w" v  p! V

( S. A: n; B( ^3 A0 v. w, b! k7 zBOOL CADOTest1App::InitInstance()
) h- y; h9 I' x% f& P  { 1 t" f2 A7 H" Q9 r
  AfxOleInit();
/ H* b1 W4 Y  o4 B# k& r7 X  ...... # X. b+ ~( F" E: i2 O! ]( w( b- V
$ n( K& m4 F! r( R! g
【2】用#import指令引入ADO类型库
' f8 R+ }/ H! D3 m" X4 k/ q, j我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到)
, n. p: b1 m( |2 Y& \#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF") ( o" K+ [/ @& U* ]$ p) y% ?4 {
这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。
9 v5 Z" T6 T/ h7 n; @  ~
. i+ {7 Y0 V& Z6 Q3 R几点说明: 2 E- X! D: A' K. p3 O6 {) O7 s
(1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改 ' W0 T6 N4 R5 e, p6 c" f; k0 D/ w
(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。 . u" v% `4 _4 T$ O. b" Q* H" U
msado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned $ o) L4 T0 b& h9 V6 [4 u0 f4 k

4 G/ G/ w. ^, z# V6 ~【3】创建Connection对象并连接数据库
! o9 b1 j8 x3 q2 l首先我们需要添加一个指向Connection对象的指针:
# h* V5 y8 y% C+ ?4 M) I( r_ConnectionPtr m_pConnection;
- U: K6 y" b3 L* G下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。 + ]) \+ e- R9 w. Z/ U

0 n8 l: W4 u  E* E  Z/ E. g& t' J8 `
BOOL CADOTest1Dlg::OnInitDialog()
* |8 |9 C2 x" M+ v' N/ F  { 7 _( V5 g0 u, B9 K
  CDialog::OnInitDialog();
* H8 M- ~3 ~) [* G3 K1 t3 Z  HRESULT hr; 2 h5 N6 Y9 I( e+ x! V
  try 4 x' q! N7 B) {4 ?2 d7 P3 _9 `
  { ! `* M. |! }1 ^5 a4 L
  hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象 & e. e4 e3 H8 N! P; _: h( U
  if(SUCCEEDED(hr))
& G2 P" x5 }; n8 E' b3 y# ~8 @8 v  { ; {/ j; y- N: D/ T3 l
  hr = m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库
( ~- _- ^" x# X1 S, ~/ ?3 U# t7 Z  ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为rovider=Microsoft.Jet.OLEDB.3.51;  }
- e% t7 w' N' K9 w6 U+ r& l. @% X  } 4 n. `2 h0 Q( B' [3 r
  catch(_com_error e)///捕捉异常 7 I4 D0 |1 }$ H+ m/ \
  {
( J( |: w  I0 M2 E  CString errormessage;
3 A. Y7 s) |' F1 R6 ?6 m  errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage());
& D: u+ ~& A$ K& s3 g. P0 l- @  AfxMessageBox(errormessage);///显示错误信息
; _% B, h% j- C+ j! N3 q  } + Z8 E: Z/ C  J" K" M

7 v; n8 E7 I( `; v$ S) f, L  w1 Q# T在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型 . l& [, @1 Q$ f' }
HRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
+ l+ N6 h% {3 j2 gConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权, 9 p! V' D4 J1 w+ a+ F: f
Options可以是如下几个常量:
$ [: {! l! O* M3 \, HadModeUnknown:缺省。当前的许可权未设置
* F: N. ]! M, Z" r, h, d: ~adModeRead:只读
6 K2 ~2 ^) W- ladModeWrite:只写
) M% M% J( `$ M% E* jadModeReadWrite:可以读写 % N# E2 N  x; d0 o/ B
adModeShareDenyRead:阻止其它Connection对象以读权限打开连接
0 r# b% G" P% U7 M$ h2 ~% _adModeShareDenyWrite:阻止其它Connection对象以写权限打开连接 / V6 g) Y% \! k
adModeShareExclusive:阻止其它Connection对象打开连接
8 J, J% t. {8 w" A6 x- x" h: m; zadModeShareDenyNone:允许其它程序或对象以任何权限建立连接
6 n  k. X6 ^* |& y6 W
/ t5 D: G2 C# h; @+ |  o: n8 d/ ^我们给出一些常用的连接方式供大家参考:
/ f. b$ t  J/ v& I( m5 G(1)通过JET数据库引擎对ACCESS2000数据库的连接 7 i5 o0 i& x& }( \+ C

- Q  Z2 o' V2 G2 [7 q7 {m_pConnection->Open(&quotrovider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown);
6 m, T% Y; X0 _. S4 Z- N) f6 f* ~2 p: Z
(2)通过DSN数据源对任何支持ODBC的数据库进行连接: 5 u5 t+ f( N) J0 m
m_pConnection->Open("Data Source=adotest;UID=saWD=;","","",adModeUnknown);
, s. D8 z, k. R
: f$ X6 s3 `* U: s" z& L; T- e+ j(3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown); 2 K' x. o3 L3 R5 F( H- |2 A
9 O0 ~" O0 `2 `9 C! Y
其中Server是SQL服务器的名称,DATABASE是库的名称
( L1 k+ H6 a' l- N6 E; N, b
) I! s% e& Z. K% y& u' |  fConnection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State ! n, a1 N. B! a5 T/ u4 @6 t9 z" C( H/ |
ConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒 $ C9 b: f; m. ^& I" Y& _
m_pConnection->Open("Data Source=adotest;","","",adModeUnknown);
  u; r" ]. H; K4 l  x. o. U. f7 v4 f* a

+ u0 q- ^. y, `4 G8 G0 F3 i" pState属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如:
9 E) }7 u. K8 g3 D' O9 kif(m_pConnection->State) 6 ?. }: X% m1 o, Q0 g
    m_pConnection->Close(); ///如果已经打开了连接则关闭它
8 K  z2 K2 S, e4 Q
3 l$ m0 n& B! T% X+ U; O3 }( v
【4】执行SQL命令并取得结果记录集 : |2 B/ j# H0 ^! P: ^2 F
为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset;   t  ]( K/ }3 i. {% ~' c$ o% K
并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset");
" h% V" g2 K' r0 p1 ASQL命令的执行可以采用多种形式,下面我们一进行阐述。
/ S' U  {& g* s5 y* O% X; t
5 x# v9 S9 E: o! H2 [( P2 k5 U, l(1)利用Connection对象的Execute方法执行SQL命令 4 b0 U; J. _% z& Q8 ?% s$ a  D
Execute方法的原型如下所示:
; e! n$ Z- p7 \% Q3 }+ z) J4 e6 N_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一:
9 a- c- w( k! k% I' W8 `$ qadCmdText:表明CommandText是文本命令
/ L, V9 z* u0 P! L0 f& W/ O% b) wadCmdTable:表明CommandText是一个表名
5 k9 m" Z/ [0 J& j  RadCmdProc:表明CommandText是一个存储过程 + \  a! d8 J' @9 \* _0 N
adCmdUnknown:未知 ) Q- i* t& ]- q7 V, ~, @* R

7 c: x5 m' v) J% LExecute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。   _variant_t RecordsAffected;
7 [$ }2 y7 B. m: O6 I7 D  ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday 2 ~3 w# f* r! q! ]# G
  m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText);
& `: T( l0 x! Z+ M9 B9 U# q  ///往表格里面添加记录
7 w- a; j. J; E. |# ?: C  m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) valueS (1, nullnullnullnullnullnullnullnullWashingtonnullnullnullnullnullnullnullnull,25,nullnullnullnullnullnullnullnull1970/1/1nullnullnullnullnullnullnullnull)",&RecordsAffected,adCmdText); : t' q# L* d7 u' G. z
  ///将所有记录old字段的值加一 7 S+ r2 `" b/ [5 p0 _- j* q
  m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText);
, F$ t/ i% n) H$ p6 o) I3 w  ///执行SQL统计命令得到包含记录条数的记录集
% ]* S1 Q. [- U; F# ^( e. R8 K  Z7 j  S  m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);
1 O0 [/ \* b' R- F4 ~$ Q  _variant_t vIndex = (long)0; # C) p" z8 @* e$ b
  _variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量
5 r8 f, P. G% |+ X  m_pRecordset->Close();///关闭记录集
& h4 n- O0 H; Z' F0 N$ ~! ^' g, S7 s* l) `  CString message; 4 A- Z1 M+ T* Z+ \7 U
  message.Format("共有%d条记录",vCount.lVal);
+ F, d& I0 k" u. I) Q3 x; z  AfxMessageBox(message);///显示当前记录条数   Q9 o/ A# w- K) u, s

8 O8 R! ^: N& L; K, J9 Y( O- Z0 \. L! E9 ~- Y3 D
(2)利用Command对象来执行SQL命令 4 J) d4 g  ~, N. `8 v
  _CommandPtr m_pCommand;
# j) K% d2 ]0 A4 C1 L  m_pCommand.CreateInstance("ADODB.Command");
0 H2 U3 t: G- _6 Z8 L  _variant_t vNULL;
6 d; z& k% A9 g$ ]0 k/ p* A! j  vNULL.vt = VT_ERROR;
: I2 Z5 `% Z) L* Q1 _% E( x1 d  vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数 6 p3 x( q' G7 G
  m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它
# ~4 x8 D; u" M' V  m_pCommand->CommandText = "SELECT * FROM users";///命令字串
/ O( D: }7 c4 q1 e; R( y! H) \  m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集
. d. P8 l# L( n  w- V" `$ k6 B% E. a
在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。
% Y; B2 v# m) U
2 Q) O4 B5 p. z8 u( ^
  q8 F8 }5 w. o+ A& _: ^( f(3)直接用Recordset对象进行查询取得记录集 1 S3 c9 R! o" f/ z. S. O
例如
2 I  O( Z; ]+ X( n2 L$ F) F4 O" l+ u9 }  v, O
  m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
0 k4 ]! a9 m, k  v' {9 S3 e' A1 Y  E* c6 y- C1 U: z
Open方法的原型是这样的: ) a4 [. Y  [/ S
HRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options )
' C1 a5 v# O. {' S2 h其中:
8 P! i! [( h/ b0 }0 d①Source是数据查询字符串
+ G" q: [9 S- j1 R2 a5 H6 N②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象)   Q: I7 G5 r3 t& J( v+ w0 q* l
③CursorType光标类型,它可以是以下值之一,请看这个枚举结构:
7 p; U6 E; y# q0 ienum CursorTypeEnum - t4 Q7 E: C# Y0 ^3 D3 |7 f2 |
{
9 o8 `; E! K: X+ ^adOpenUnspecified = -1,///不作特别指定
$ C4 _: i  a) b, c/ @" y) ~  @adOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用 9 H# |: h. k+ N2 M
adOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。
/ h7 I/ H0 `) n; f5 F0 k. B  F3 z7 t2 DadOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。
* h+ p0 J. o. _+ c2 L$ ?adOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。
% \8 E  C% F% ~8 D, H% }}; 9 @' g& l; i+ {! t
④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
: M6 l' \! O; zenum LockTypeEnum . G- Z( }7 Y# R1 K
{ ' I8 z8 a* q' F- F3 @; x" ?" e
adLockUnspecified = -1,///未指定
0 L$ J0 U+ \) N) j5 t. Y: y8 qadLockReadOnly = 1,///只读记录集
$ o9 o; D: g% J) T$ OadLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制
# E; J+ k( k' H  RadLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作 ! D- h( a* p4 d) _- Q. P
adLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。 6 q! @* w8 x2 S6 V9 Z  |5 o! X+ S: H, }
}; % T$ \+ x5 j) Z
⑤Options请参考本文中对Connection对象的Execute方法的介绍
  }( G) a3 P, E0 v0 I$ v% r( K/ a
* G6 a& U4 N( V/ [3 ^, P. g9 I% h+ ]6 H8 r# `$ ~
【5】记录集的遍历、更新 - v) Y% ^8 {: D1 Q( d/ ?
根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday 4 n* m- `8 J8 D- v7 W0 i6 H
以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。 . I. \* V# y4 o! D* z9 M6 ]

, {  t( |2 s/ T% Q, E2 Q
9 l# A7 q( P2 t" e+ S_variant_t vUsername,vBirthday,vID,vOld;
  k. K) Y3 e) E; @, P1 ]  I/ H_RecordsetPtr m_pRecordset; 0 v4 e! R( z' h3 t; r2 N
m_pRecordset.CreateInstance("ADODB.Recordset");
1 b2 G7 Z- q+ Km_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
7 }' j$ j  g# `( J) o; _+ M3 Z5 swhile(!m_pRecordset->adoEOF)///这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗? $ Y$ I" W5 f. v
{ / l$ q5 n  e% d5 m8 I0 e) M# U% s
vID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行 : p  a/ B6 {9 S4 |( T
vUsername = m_pRecordset->GetCollect("username");///取得username字段的值
' g$ G/ h, C' b, \. Q( L+ J5 tvOld = m_pRecordset->GetCollect("old"); ( ]: \' E9 A- t( e+ C% E! B+ T
vBirthday = m_pRecordset->GetCollect("birthday");
  E* b* e( K: x9 u& E# F///在DEBUG方式下的OUTPUT窗口输出记录集中的记录
- w9 p) f8 V: _. f) A& g+ A  k+ [1 E$ `if(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL) # L  I: v. D/ F6 Q3 ?1 K4 X
  TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday); 1 g; m7 }& n6 g; X
m_pRecordset->MoveNext();///移到下一条记录 - J# U. H6 U/ `. k) r, l. Q
}
' i; C' F8 t9 e' Vm_pRecordset->MoveFirst();///移到首条记录 ' t( ]2 w  r9 h% {$ G) o$ @
m_pRecordset->Delete(adAffectCurrent);///删除当前记录
5 x( `- u/ J9 e! @9 g! Y0 y5 O///添加三条新记录并赋值
( T3 Z8 f# b/ y1 a# hfor(int i=0;i<3;i++) 9 o6 h! i4 R7 B+ U
{ 8 U( D0 Q" j5 x7 S9 }6 A
m_pRecordset->AddNew();///添加新记录 ( Y) T( e6 J/ @& \
m_pRecordset->PutCollect("ID",_variant_t((long)(i+10))); # a  E7 D% n9 d  f
m_pRecordset->PutCollect("username",_variant_t("叶利钦")); / u7 g8 \( \( Y# B0 b
m_pRecordset->PutCollect("old",_variant_t((long)71)); 2 m5 q: c* e2 P% |3 x7 Z
m_pRecordset->PutCollect("birthday",_variant_t("1930-3-15"));
( n" e' G* o  j- J+ ^; |}
9 B8 [1 P; p1 z& [m_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处   N# f" @' b& }7 t! s! w( Y- i
m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄 % `# L8 e# L& W
m_pRecordset->Update();///保存到库中 4 I- Y9 p8 G7 {  i  k" f

8 I. {  |' @" }( u. [/ \【6】关闭记录集与连接
' }9 {% \3 L  [/ r8 F/ ~) ~; D记录集或连接都可以用Close方法来关闭
9 W0 [2 I9 E5 G% f# _2 K& l" P# a      m_pRecordset->Close();///关闭记录集 2 l8 I; G4 X' O; I' ?) T# D1 ~3 W
     m_pConnection->Close();///关闭连接 ; R' j4 m0 c  k' |
% m! i. C5 f' W# o/ V  H3 |2 a
至此,我想您已经熟悉了ADO操作数据库的大致流程,也许您已经胸有成竹,也许您还有点胡涂,不要紧!建议你尝试写几个例子,这样会更好地熟悉ADO,最后我给大家写了一个小例子,例子中读出所有记录放到列表控件中、并可以添加、删除、修改记录。 ! l* V( l4 O8 T3 _8 P( a8 o
点这里下载示例代码
# ^0 }  d% ^; N% S
2 h+ @% L& [+ d+ \& Z* h后记:限于篇幅ADO中的许多内容还没有介绍,下次我们将详细介绍Recordset对象的属性、方法并解决几个关键的技术:绑定方式处理记录集数据、存储过程的调用、事务处理、图象在数据库中的保存与读取、与表格控件的配合使用等。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2026-10-2 06:30 , Processed in 0.019675 second(s), 14 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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