English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french

简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE คนไทย Français русский

回答

收藏

4.3 带有存款/提款逻辑的合同

开源社区 开源社区 9818 人阅读 | 0 人回复 | 2025-03-08

在这一课中,我们将练习执行更多不同的命令、创建函数,甚至从合约内部发送信息。我们将在现有合约的基础上完成所有这些工作。
我们的计划是什么?让我们来分析一下:
  • 我们的合约对指令的要求将变得更加严格,我们将引入一个新的操作码,用于向合约存入资金的逻辑。
  • 我们要引入的另一个操作代码是提取资金。在提取资金过程中,资金实际上是以消息的形式发送到地址的,因此我们将学习如何在合约中发送消息
  • 任何以未知操作代码到达的资金都将返回给发送方
  • 我们将在存储中引入一个新值--合约所有者。只有合约所有者才能提取资金。我们还将在函数中分离存储加载和写入逻辑,以保持主代码的整洁

    4 ~2 @6 @' m1 W/ }% D" E( D: a  ]5 R
准备好了吗?就绪。开始!
分离存储管理逻辑
首先,让我们来处理合约所有者存储中的数据(我们需要记住,我们必须在启动合约时将该地址放入存储中)。我们还将创建两个新函数-load_data 和 save_data:
  1. (int, slice, slice) load_data() inline {
    ) Y$ q/ P4 a) [# s* Y3 J5 ~! k
  2.   var ds = get_data().begin_parse();3 r1 t& J: f3 x- G8 b  l, e8 @
  3.   return (5 e) [" d# q, ?4 W" s: d
  4.           ds~load_uint(32), ;; counter_value* f6 k2 D* b5 A4 x
  5.            ds~load_msg_addr(), ;; the most recent sender
    5 L/ U" E6 j3 E( ]5 [
  6.             ds~load_msg_addr() ;; owner_address2 V) b' {/ D6 S
  7.   );1 ]  F0 L. ?6 z4 s7 |7 H% z
  8. }
    - t, S. @4 V9 y: k$ ?  M" ?9 \
  9. ; w7 c3 }8 S% ^. y0 t
  10. () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {
    4 O0 M1 ^, c. E/ Q
  11.   set_data(begin_cell(); D. @: n- g; a: ?0 L  O6 b
  12.     .store_uint(counter_value, 32) ;; counter_value% W  a& G) V3 v2 }2 h# D. O
  13.     .store_slice(recent_sender) ;; the most recent sender
    & ~0 D) z1 D( t) w1 K' F: O, f
  14.     .store_slice(owner_address) ;; owner_address
    - a. I7 J( F8 e4 _; K
  15.     .end_cell());' e- N5 W0 P4 M  X  Y4 Q% H) Y
  16. }
复制代码
这里有几点需要注意:
  • inline 指定符。你已经对指定符有所了解。如果一个函数有内联指定符,那么在调用该函数的每一个地方,它的代码实际上都会被替换。禁止在内联函数中使用递归调用。
  • 为什么 load_data() 没有 impure 函数指定符?我们在第 3 章中已经讨论过这个问题,现在让我们来复习一下。答案是,因为这个函数不会影响合约的状态。它是只读的。

    ! {/ N/ V! O0 t& e- u
我们将在代码中进一步使用这些函数:
  1. var (counter_value, recent_sender, owner_address) = load_data();0 G4 G* h+ n) Q( D' D

  2. , r  ~0 G& }, D; F# Q) @' J8 t
  3. save_data(counter_value,recent_sender,owner_address);
复制代码
有几点需要注意:
  • 我们还更新了 get_the_latest_sender 函数,以返回所有者地址
  • 如果从信息正文中读取的操作码没有触发任何 if 语句 - 我们会抛出一个代码为 777 的错误。这个数字可以自己选择,但要确保它不会与 official error exit codes 相同. (https://ton.org/docs/learn/tvm-instructions/tvm-exit-codes) exit_code 如果大于 1,则视为错误代码,因此退出时如果出现这样的代码,可能会导致交易回退/反弹。
  • 我们使用函数 return() 成功退出合约执行

    ! g: o: l& @; y
总的来说,代码看起来很简洁,不是吗?
存款和取款
对我们来说,存款 (op == 2) 非常简单,因为在这种情况下,我们只需成功完成执行,使得资金被接受。否则,资金将退回给发件人。
  1. if (op == 2) {3 D9 q/ [5 O/ ?* a: Z" u- m
  2.     return();
    6 g* p, Q: Q/ p* \- ^5 |; Q
  3. }
复制代码
不过,取款时情况会变得复杂一些。我们必须将发件人地址与智能合约的所有者地址进行比较。让我们来看看,为了实现这个逻辑,我们需要知道哪些事情:
  • 要比较所有者地址和发送者地址,我们使用 FunC 标准函数 equal_slice_bits()
  • 我们使用 throw_unless() 函数在比较结果为 false. 还有另一种通过错误的方法-throw_if(),如果传入该函数的条件返回 true,该函数就会抛出错误.
  • 带有此操作的消息正文还需要有一个整数,说明要求提取的金额。我们将此金额与合约的实际余额进行比较(标准 FunC 函数 get_balance())。
  • 我们希望我们的合约总是有一些资金,能够支付租金和必要的费用(在第 1 章中了解更多关于费用的信息),因此我们需要设置合约中必须保留的最低金额,如果请求的金额不允许,则会抛出错误。
  • 最后,我们需要学习如何通过内部信息从合约内部发送代币
    ; W; G3 w3 @6 H& P, n3 j. K0 ?9 `

3 y" ]; ~. ~7 y* V; M1 a5 U/ a: f
我们看看这一切是如何实现的。
首先,我们设置最小所需存储空间的常数:
  1. const const::min_tons_for_storage = 10000000; ;; 0.01 TON
    * p. c8 z+ w* X9 q% T$ U
复制代码
然后,我们实现提款的逻辑。它看起来是这样的:
$ |4 {8 I7 K, x
  1. if (op == 3) {, g6 |0 i& d# B) v0 E  a/ f3 d
  2.     throw_unless(103, equal_slice_bits(sender_address, owner_address));' L: R% `# R" n# i: i- V. N
  3. 3 c- E. c. p+ T3 U# i2 L9 A9 u
  4.     int withdraw_amount = in_msg_body~load_coins();
    $ y7 \+ U' d  \7 G
  5.     var [balance, _] = get_balance();$ e0 B4 O: }6 C
  6.     throw_unless(104, balance >= withdraw_amount);
    2 b. G, O) V& ^7 T9 O4 X8 V
  7. & G, U5 g5 v( E  \2 h
  8.     int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);- A8 }$ o6 H9 m. Q/ K* O6 ]2 V

  9. ) X# R; B! x. N
  10.     $ k2 X, O9 S) H+ x) N5 z/ o, ]
  11.     ;; TODO: Sending internal message with funds
    & H( z' r6 L) {/ c/ \: D& F
  12.     , k) D5 i; l3 h  T+ y& a7 ]! K
  13.     return();; V4 W: S  E8 [. u, j
  14. }
复制代码
如您所见,我们正在读取提款的代币数量(将存储在我们的 in_msg_body 在操作代码之后,我们将在下一课中完成) 检查余额是否大于或等于提款金额。
我们还使用了一种很棒的技术,确保在合约中保留最低存储金额。
让我们来详细谈谈这个发送实际资金的逻辑。
发送内部信息
send_raw_message 是一个标准函数,它接受一个带有信息的Cell 和一个包含 mode 和 flag 的 integer(整数). 目前有 3 种模式和 3 个信息标志。您可以将单个模式与多个(也可以一个都没有)标志相结合,以获得所需的模式。简单地说,组合就是获得它们的值之和。关于模式和标志的说明,请参阅下面的表格  this documentation part.
在我们的示例中,我们要发送常规消息并单独支付转账费用,因此我们使用模式 0 和标志 +1 得到 mode = 1.
我们在将其传递到 send_raw_message() 之前编写的信息Cell,可能是目前你需要了解的最高级的内容。
我们将花一点时间来确保这部分内容变得清晰明了。不过,在开始之前,请允许我给你们两个建议:
  • 首先--习惯数据在Cell中的存储方式。 这就是 serialization. 此时,您需要熟悉并习惯这一点,因为您会经常将数据序列化到Cell中,因为所有数据都存储在Cell中。
  • 第二-适应 TON documentation. 这几乎是处理所有这些序列化结构的唯一方法,因为要记住它们非常困难。
    * o; Y) w" f/ L3 `/ U
您可能会发现一个非常有用的东西 old documentation portal, 因为它有一些主题是以一种非常基本且良好的方式进行阐述。
让我们来分析一下要传入 send_raw_message 的信息Cell的结构。正如你已经理解的那样,我们需要在Cell中放入若干位bits,而哪个位bits负责哪项工作是有一定逻辑的。
信息Cell以 1 位前缀 0 开始,然后是三个 1 位标志,即是否禁用即时超立方路由(目前始终为 true)、如果处理过程中出现错误是否应跳转消息、消息本身是否是跳转的结果。然后,源地址和目标地址被序列化,接着是信息值和四个与信息转发费用和时间有关的整数。
如果从智能合约发送信息,其中一些字段将被改写为正确的值。特别是,验证器将重写 bounced, src, ihr_fee, fwd_fee, created_lt 和 created_at. 这意味着两件事:第一,另一个智能合约在处理信息时可以信任这些字段(发件人可以不伪造源地址、退回标志等);第二,在序列化过程中,我们可以在这些字段中加入任何有效值(无论如何,这些值都会被覆盖).
信息的直接序列化如下(摘自 documentation portal):
  1. var msg = begin_cell(); n. ]% K0 L% O) U
  2.     .store_uint(0, 1) ;; tag
    + H; K/ @; A7 l( u
  3.     .store_uint(1, 1) ;; ihr_disabled- U% @. ^; P3 n- e& x
  4.     .store_uint(1, 1) ;; allow bounces
    . M/ |. E" G% ^, h( Q
  5.     .store_uint(0, 1) ;; not bounced itself8 m- h- u8 [$ f% w% l# m3 A
  6.     .store_slice(source) ;; ex. addr_none& Q0 k, p/ h% V# D( L$ O
  7.     .store_slice(destination)
    ! x6 f& I! a$ n8 a
  8.     ;; serialize CurrencyCollection (see below)
    7 P+ ?2 Q% l- P$ x9 i
  9.     .store_coins(amount)
    $ g* ?! T% v2 \* x
  10.     .store_dict(extra_currencies)3 s+ G7 W, v& h9 V
  11.     .store_coins(0) ;; ihr_fee
    # h+ @  h- C7 a, w
  12.     .store_coins(fwd_value) ;; fwd_fee
    3 r- J  w, l! I0 r+ `
  13.     .store_uint(cur_lt(), 64) ;; lt of transaction) x- e2 i+ c. o$ K. c2 \5 i
  14.     .store_uint(now(), 32) ;; unixtime of transaction0 T- O; o: {+ F
  15.     .store_uint(0,  1) ;; no init-field flag (Maybe)
    ' U0 j5 S3 c" i- ]+ t, L
  16.     .store_uint(0,  1) ;; inplace message body flag (Either)
    ( l; D* W5 p9 t% o  m: `/ ]% o9 t
  17.     .store_slice(msg_body)! Q  p% r8 m. T! a1 b  \% L" t
  18.   .end_cell();
复制代码
不过,开发人员通常会使用快捷方式,而不是逐步序列化所有字段。因此,让我们举例说明如何从智能合约中发送信息:

1 J, r& C( g$ x1 I9 f; B% m2 @
  1. var msg = begin_cell()
    & f2 S& i, y. j
  2.     .store_uint(0x18, 6)2 _3 a2 Q1 \4 a3 G) F
  3.     .store_slice(addr)
    / o8 i! c" t- M: r8 _* b. L
  4.     .store_coins(grams)
    / U: r9 m  A4 V# n& V3 s
  5.     .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)( s* S6 a/ Z5 j) }1 U
  6.     .store_uint(op_code, 32)
    9 u/ {' i+ D4 e6 T) i* W
  7.     .store_uint(query_id, 64);
    ; T* {% {# [6 `2 E3 f! a
  8.    
    ! p$ A0 k! \0 r9 u
  9. send_raw_message(msg.end_cell(), mode);
复制代码
我们仔细看看到底发生了什么。
  • .store_uint(0x18, 6)! }( ]# d" j* ]
    我们开始组成一个Cell时,将 0x18 值转换成 6 位,即 0b011000 如果我们从十六进制翻译过来。这是什么?
    - q4 s% L  U; e9 p9 N( ]  ^) k& t0b011000
    • 第一位是 0-1 位前缀,表示它是 int_msg_info (内部信息)。
    • 然后有 3 个位 1、1 和 0,即
      • 禁用即时超立方路由(我们将不详细介绍它是什么,因为它太基础,您在编写合约时暂时用不上)
      • 信息可能被退回
      • 信息本身不是退回的结果。
        ' t+ C. Q9 c6 f! M
    • 然后应该有发件人地址,但由于它无论如何都会被重写(当验证器将发件人的实际地址放在这里时,就像我们上面讨论的那样),因此任何有效地址都可以存储在这里。最短的有效地址序列化是 addr_none,它序列化为一个两位字符串 00。
      - H) c9 f3 d% D& x& C$ w
  • 因此,.store_uint(0x18, 6) 是对标签和前 4 个字段进行序列化的优化方法。
  • .store_slice(addr) - 这一行将目标地址序列化。
  • .store_coins(grams) - 这一行只是将代币的数量序列化(grams 只是一个带有代币数量的变量)。
  • .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - 这个问题很有趣。从技术上讲,我们只是在Cell中写入了大量的零(即零的数量等于 1 + 4 + 4 + 64 + 32 + 1 + 1)。为什么会这样?我们知道,这些值的消耗有一个清晰的结构,这意味着我们放在这里的每一个 0 都是有原因的。- T4 ]  n  q/ M% c; m) i
    有趣的是,这些零有两种工作方式--其中一些被设置为 0,因为无论如何验证器都会重写值;另一些被设置为 0,因为目前还不支持该功能(如额外货币)。
    5 z& j2 A0 H3 ]2 n% n6 x) b为了确保我们理解为什么会有这么多零,让我们来分析一下它的预期结构:
    • 第一位代表空的额外货币字典。
    • 这样我们就有了两个 4 位长的字段。因为 ihr_fee 和 fwd_fee 将被覆盖,因此我们不妨将其归零。
    • 然后,我们把 0 添加到 created_lt 和 created_at 字段。 这些字段也将被覆盖;不过,与费用相比,这些字段的长度是固定的,因此被编码为 64 位和 32 位长字符串。
    • 下一个零位表示没有初始字段。
    • 最后一个零位表示 msg_body 将就地序列化。 这基本上表示 msg_body 是否带有自定义布局。

        M5 t( }% X! p( _
  • .store_uint(op_code, 32).store_uint(query_id, 64); 这部分是最简单的--我们传递的是一个具有自定义布局的信息正文,这意味着只要接收方知道如何处理,我们就可以在其中放置任何数据。

    3 m% g2 G. T  N

" K* E0 p! D" c  a$ r# b) o3 ^我们看看如何将其应用到我们的提款代码中:
" k+ k$ o9 e1 t  g; q% ^% V
  1. if (op == 3) {
    ; r1 r: `7 v, _7 [. b+ {+ ?" _0 i7 l6 f
  2.     throw_unless(103, equal_slice_bits(sender_address, owner_address));
    # g* `2 c+ \  |6 w2 M( q% r8 b
  3. # W( S  L9 }- B; l
  4.     int withdraw_amount = in_msg_body~load_coins();
    & E% d2 J7 ~0 r" Y) }6 J+ H7 F9 A9 J
  5.     var [balance, _] = get_balance();7 C% N! R, f" x6 z- `
  6.     throw_unless(104, balance >= withdraw_amount);
    ( m+ |" Q5 @5 p5 n; X, `
  7. 4 U: j' B! L: K" @, m
  8.     int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);  S' l, Z. n1 d! _
  9.     0 y6 P; h" b7 X" R- W
  10.     int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)3 C5 V/ c& L% S' ]2 ~! f3 u
  11.     # k$ i' R! h8 W8 n
  12.     var msg = begin_cell()/ A9 n2 Q6 |: s9 ?- w8 }
  13.         .store_uint(0x18, 6)2 T4 E2 j9 ]. \5 ^, B
  14.         .store_slice(sender_address)
    4 ^: X1 |4 }, N; ~: E; x$ Q' y9 R
  15.         .store_coins(return_value)
    4 c4 t% I' z9 Y  o7 l9 c9 z
  16.         .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
    ( q1 \" H3 O! ~7 T0 `* r( Y' }
  17.    
      k& B7 c% e1 {" c6 ^7 ^! Y' q6 z- Y
  18.     send_raw_message(msg.end_cell(), msg_mode);
    ! D# T8 B6 ]% X3 X8 `
  19.    
    " h; n$ X& M5 N5 k/ w
  20.     return();& n, K: A! ?& i" P. P7 V# T* \
  21. }
复制代码
敲定我们的合约代码
最后一件事是添加一个新的 getter 方法,用于返回合约余额:
  1. int balance() method_id {
    ( D2 n, u% C) Z8 Q9 o
  2.   var [balance, _] = get_balance();
    ( \/ N7 o0 J, i' r  j6 g+ m0 |
  3.   return balance;
    # Z- D- c: V8 q! ?
  4. }
复制代码
我们再来看看最终代码的样子:! ]# }. l2 a  h2 P* F
  1. #include "imports/stdlib.fc";/ m/ `( O  ^* W' z* f1 |0 Y) f
  2. 2 d7 D* ^, `3 y( V
  3. const const::min_tons_for_storage = 10000000; ;; 0.01 TON6 e1 @6 d% Z2 C% y; I1 L( L' w6 T
  4. % t1 Z  e7 @3 \% Y! i/ s; _- r
  5. (int, slice, slice) load_data() inline {) |9 d& b3 b; s$ p
  6.   var ds = get_data().begin_parse();  U% S0 [  w) h$ u
  7.   return (
    5 _6 P) R- T; @7 U/ N1 n
  8.     ds~load_uint(32), ;; counter_value
    % ~3 e4 F6 C( o- V! u' t9 n5 P3 Q
  9.     ds~load_msg_addr(), ;; the most recent sender$ x. z5 y6 G; A5 D0 k, O
  10.     ds~load_msg_addr() ;; owner_address3 A. x$ F( z& j% U
  11.   );* k8 x1 S" H" V
  12. }( c9 @$ |# r% j
  13. 3 F' T3 ^4 k' Z1 @- R7 Q
  14. () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {4 ?* G0 R) O# L' }0 b4 n
  15.   set_data(begin_cell()
    5 P1 V, q' C% W- O, P3 F. b
  16.     .store_uint(counter_value, 32) ;; counter_value
    ) o( A+ t4 f) z. G- A$ e
  17.     .store_slice(recent_sender) ;; the most recent sender
    ' f0 n" S; P0 L
  18.     .store_slice(owner_address) ;; owner_address
    ; s1 z2 i" A' C3 l; \  K
  19.     .end_cell());; f! k- c! O. P5 W7 X0 K: U
  20. }
    4 {3 F  j2 T( f; }# Q* n

  21. , e* U# b, ^" R! _9 c, U; W
  22. () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
    + L, T1 S$ |4 V
  23.   slice cs = in_msg.begin_parse();
    " T# U5 Q& U/ C8 O9 b1 Q
  24.   int flags = cs~load_uint(4);
    3 f7 d" c' @+ G+ R
  25.   slice sender_address = cs~load_msg_addr();
    7 `2 E/ n# ]. l' _2 |
  26. . _( ]3 h: P5 c- J8 k; ^5 D3 q8 J
  27.   int op = in_msg_body~load_uint(32);
    ' X8 H7 Q) R4 A

  28. + c% z2 }5 O! T7 S+ G. X
  29.   var (counter_value, recent_sender, owner_address) = load_data();2 u" m7 \1 _1 [: V4 r1 D: n7 w
  30.        
    4 L5 E( A+ }: _/ h! O$ c. z$ e
  31.   if (op == 1) {
    7 o6 d8 o& q# E- z+ S
  32.     save_data(counter_value + 1, sender_address, owner_address);1 P- _5 u5 N, n; G
  33.     return();- P4 D; \% T7 b+ I# Q
  34.   }
    ' f. ~/ n" N) W9 z2 [

  35. ( W! q- Q) Z, Z; k' X  Q0 w% N
  36.   if (op == 2) {
    ( I/ L: Q8 J' I7 s
  37.     return();
    0 A4 w. M  L" }& b: L& Y7 \
  38.   }2 i% N% d' \6 j. f
  39. 2 U2 `$ ^! ?2 R4 n# W4 E
  40.   if (op == 3) {
    ! O2 u) S, d" T3 J9 t( |. u
  41.         throw_unless(103, equal_slice_bits(sender_address, owner_address));
    4 k; h$ R! Z# `$ A9 E+ l6 @
  42. " f3 m2 w& X$ x" ^
  43.         int withdraw_amount = in_msg_body~load_coins();
    + S/ M( i) c- w" Z$ O
  44.         var [balance, _] = get_balance();3 r0 q" q% R& M
  45.         throw_unless(104, balance >= withdraw_amount);
    % r/ |9 ^0 \% h2 V6 S  N: i( W% M# ^
  46. 5 J+ e( n5 Y$ C1 b4 X( [
  47.         int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);
    * Y1 h0 y+ [: n5 i% _1 A+ O* _
  48. 7 _3 h! Z. J/ A/ D3 S# ^7 Q/ b
  49.         int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)7 q. v( x! A, v! O' ]5 l

  50. 3 d) k+ b8 a$ Q  @7 x
  51.         var msg = begin_cell()1 G* P: k' M( ]' A$ D
  52.             .store_uint(0x18, 6)
    : |% ~5 i1 p% u. |. ]. o9 ?
  53.             .store_slice(sender_address)8 X/ \2 V, a& ]2 ?: @, y2 m4 |
  54.             .store_coins(return_value)7 |  O! z( x' f5 K/ ?$ Y& p4 k& Z
  55.             .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);& b! U3 K" X0 F" \
  56. 0 u3 c7 S: h& J5 H" b% e/ h
  57.         send_raw_message(msg.end_cell(), msg_mode);- q) n; s! {7 k/ ]" y! I* y" h' D

  58. 0 D' [  ?/ J' d! p- V
  59.         return();
    / W" K' x8 @. W5 [
  60.     }
    ( ]. ~# t, c( [4 m5 `
  61. 7 Y# n+ G/ }2 K* R' G
  62.   throw(777);
    + m" P- ~0 e6 L6 `
  63. }; P$ o1 ^7 ~/ g' s& v  B

  64. . f- X" N- E% L- H) J
  65. (int, slice, slice) get_contract_storage_data() method_id {+ l" t$ V! M: u" I& r
  66.   var (counter_value, recent_sender, owner_address) = load_data();# J! _) Z% ~8 C: {' ?
  67.   return (& Y7 K$ P0 ?: Y/ @% V( u
  68.     counter_value,6 w; B2 D4 ~+ l& ?' D8 D
  69.     recent_sender,7 J! P/ T( l/ f1 K
  70.     owner_address: T1 \% N4 G# C  ~
  71.   );- Y% R5 U' e/ R% l5 U2 E1 G. s) m9 j
  72. }# z& q' _% W; N; W
  73. 6 [# N. E( A& |+ s$ S
  74. int balance() method_id {
    + C9 y2 i5 n  T1 ^  z; \
  75.   var [balance, _] = get_balance();4 U1 t* w; O, |) q- D( P
  76.   return balance;
    ! o! \" B8 D# i
  77. }
复制代码
我们检查一下我们的代码是否能用 yarn compile 然后 让我们开始为更新后的合约编写测试。
/ x8 }( w: x4 D% Q) y8 i8 S5 ^4 v6 ~+ }- W3 {0 v# p
2 D5 _6 r2 y' ]- _7 o
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则