Flutter: 项目开发 — ShopApp 功能实现

设计是图纸,开发是施工——这一课把蓝图变成可运行的应用。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个功能遗漏的故事

(1) 痛点:功能做完了但串不通

Bob 完成了所有模块的独立开发,但集成时发现:购物车加购后详情页的"已加购"状态没更新(两个独立的 setState);商品库存被超卖(Firestore 读写没有事务);支付成功后订单状态没自动更新。每个问题都要跨 3-4 个文件修改。

(2) Riverpod 全局状态串联的解法

Riverpod 让所有模块共享同一个状态源:购物车状态变化时,详情页、角标、总价自动更新;Firestore 事务保证库存不超卖。

(3) 收益:模块无缝集成

Bob 用 Riverpod 串联所有模块后,跨模块联动自动生效,不再需要手动同步状态。


3. 商品模块实现

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Product Notifier(异步加载 + 搜索 + 分类)

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ 依赖:flutter_riverpod: ^2.0.0, riverpod_annotation: ^2.0.0
// Product / ProductRepository / productCacheProvider / productRepositoryProvider
// selectedCategoryProvider 来自 ShopApp 项目,完整定义见第26/27课

@riverpod
class Products extends _$Products {
  @override
  Future<List<Product>> build() async {
    return _loadFromCacheOrApi();
  }

  Future<List<Product>> _loadFromCacheOrApi() async {
    final cache = ref.read(productCacheProvider);
    final cached = await cache.getProducts();
    if (cached.isNotEmpty) return cached;
    return _fetchFromApi();
  }

  Future<List<Product>> _fetchFromApi() async {
    final repo = ref.read(productRepositoryProvider);
    final products = await repo.getProducts();
    await ref.read(productCacheProvider).saveProducts(products);
    return products;
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchFromApi());
  }

  Future<void> search(String query) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final repo = ref.read(productRepositoryProvider);
      return repo.searchProducts(query);
    });
  }

  Future<void> filterByCategory(String category) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final repo = ref.read(productRepositoryProvider);
      return repo.getProducts(category: category);
    });
  }
}

// Derived: filtered products
@riverpod
List<Product> filteredProducts(FilteredProductsRef ref) {
  final productsAsync = ref.watch(productsProvider);
  final category = ref.watch(selectedCategoryProvider);
  return productsAsync.valueOrNull?.where((p) =>
    category == 'All' || p.category == category).toList() ?? [];
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Firestore 实时评论

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

// ⚙️ 依赖:flutter_riverpod: ^2.0.0, cloud_firestore: ^4.0.0
// Review 来自 ShopApp 项目,完整定义见下方

// 简化版 Review 实体
class Review {
  final String id;
  final String productId;
  final String userName;
  final int rating;
  final String comment;
  final DateTime createdAt;

  const Review({
    required this.id, required this.productId, required this.userName,
    required this.rating, required this.comment, required this.createdAt,
  });

  factory Review.fromFirestore(DocumentSnapshot doc) {
    final data = doc.data() as Map<String, dynamic>;
    return Review(
      id: doc.id,
      productId: data['productId'] ?? '',
      userName: data['userName'] ?? 'Anonymous',
      rating: data['rating'] ?? 0,
      comment: data['comment'] ?? '',
      createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
    );
  }
}

@riverpod
Stream<List<Review>> productReviews(ProductReviewsRef ref, String productId) {
  return FirebaseFirestore.instance
    .collection('reviews')
    .where('productId', isEqualTo: productId)
    .orderBy('createdAt', descending: true)
    .limit(50)
    .snapshots()
    .map((s) => s.docs.map((d) => Review.fromFirestore(d)).toList());
}

// Widget
class ReviewSection extends ConsumerWidget {
  final String productId;
  const ReviewSection({super.key, required this.productId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final reviewsAsync = ref.watch(productReviewsProvider(productId));
    return reviewsAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
      data: (reviews) => Column(children: [
        Text('Reviews (${reviews.length})', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
        const SizedBox(height: 8),
        ...reviews.map((r) => ListTile(
          leading: CircleAvatar(child: Text(r.userName[0])),
          title: Row(children: [
            Text(r.userName),
            const SizedBox(width: 8),
            ...List.generate(5, (i) => Icon(Icons.star, size: 14,
              color: i < r.rating ? Colors.amber : Colors.grey[300])),
          ]),
          subtitle: Text(r.comment),
        )),
      ]),
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

4. 购物车模块实现

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:CartNotifier(完整版 + 持久化)

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ 依赖:flutter_riverpod: ^2.0.0, riverpod_annotation: ^2.0.0
// Product / CartItem / cartStorageProvider / ShopAnalytics 来自 ShopApp 项目
// CartItem / Address 完整定义见第26课

@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() {
    _loadFromStorage();
    return [];
  }

  Future<void> _loadFromStorage() async {
    final storage = ref.read(cartStorageProvider);
    final items = await storage.loadCart();
    if (items.isNotEmpty) state = items;
  }

  Future<void> _persist() async {
    await ref.read(cartStorageProvider).saveCart(state);
  }

  void addItem(Product product, {String? size, String? color}) {
    final idx = state.indexWhere((i) =>
      i.product.id == product.id && i.selectedSize == size && i.selectedColor == color);
    if (idx >= 0) {
      state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
    } else {
      state = [...state, CartItem(product: product, quantity: 1, selectedSize: size, selectedColor: color)];
    }
    _persist();
    ShopAnalytics.logAddToCart(product, 1);
  }

  void updateQuantity(int index, int quantity) {
    if (quantity <= 0) { removeAt(index); return; }
    state = [for (int i = 0; i < state.length; i++) i == index ? state[i].copyWith(quantity: quantity) : state[i]];
    _persist();
  }

  void removeAt(int index) {
    state = [...state]..removeAt(index);
    _persist();
  }

  void clear() { state = []; _persist(); }
}

@riverpod
double cartTotal(CartTotalRef ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0.0, (sum, item) => sum + item.lineTotal);
}

@riverpod
int cartItemCount(CartItemCountRef ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0, (sum, item) => sum + item.quantity);
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

5. 支付模块实现

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Stripe 支付集成

DART
import 'package:dio/dio.dart';
import 'package:flutter_stripe/flutter_stripe.dart';

// ⚙️ 依赖:dio: ^5.0.0, flutter_stripe: ^10.0.0(添加到 dependencies)
// Stripe 后端需自行部署,参考 https://stripe.com/docs

class PaymentService {
  static Future<bool> processPayment({
    required double amount,
    required String currency,
    required String paymentMethodId,
  }) async {
    try {
      // Create payment intent via backend
      final response = await Dio().post('https://api.shopapp.com/v1/payments/create-intent', data: {
        'amount': (amount * 100).toInt(), // Stripe uses cents
        'currency': currency.toLowerCase(),
        'payment_method_id': paymentMethodId,
      });

      final clientSecret = response.data['client_secret'];

      // Confirm payment
      final paymentIntent = await Stripe.instance.confirmPayment(
        paymentIntentClientSecret: clientSecret,
        data: PaymentMethodParams.cardFromMethodId(
          paymentMethodData: PaymentMethodDataCardFromMethod(id: paymentMethodId),
        ),
      );

      return paymentIntent.status == PaymentIntentsStatus.Succeeded;
    } catch (e) {
      return false;
    }
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:订单状态机

DART
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

// ⚙️ 依赖:cloud_firestore: ^4.0.0(添加到 dependencies)
// CartItem / Address / Product 完整定义见第26课

enum OrderStatus { pending, confirmed, processing, shipped, delivered, cancelled }

extension OrderStatusX on OrderStatus {
  bool get canCancel => this == OrderStatus.pending || this == OrderStatus.confirmed;
  bool get canTrack => this == OrderStatus.shipped;
  String get label => switch (this) {
    OrderStatus.pending => 'Pending',
    OrderStatus.confirmed => 'Confirmed',
    OrderStatus.processing => 'Processing',
    OrderStatus.shipped => 'Shipped',
    OrderStatus.delivered => 'Delivered',
    OrderStatus.cancelled => 'Cancelled',
  };
  Color get color => switch (this) {
    OrderStatus.pending => Colors.orange,
    OrderStatus.confirmed => Colors.blue,
    OrderStatus.processing => Colors.indigo,
    OrderStatus.shipped => Colors.teal,
    OrderStatus.delivered => Colors.green,
    OrderStatus.cancelled => Colors.red,
  };
}

// Firestore order creation with transaction
Future<String> createOrder(List<CartItem> items, Address address, String userId) async {
  final db = FirebaseFirestore.instance;
  final orderId = 'ORD-${DateTime.now().millisecondsSinceEpoch}';

  await db.runTransaction((txn) async {
    // Check stock for all items
    for (final item in items) {
      final productDoc = await txn.get(db.collection('products').doc(item.product.id.toString()));
      final stock = productDoc.data()?['stock'] ?? 0;
      if (stock < item.quantity) throw Exception('Insufficient stock for ${item.product.name}');
      txn.update(db.collection('products').doc(item.product.id.toString()), {
        'stock': stock - item.quantity,
      });
    }
    // Create order
    txn.set(db.collection('orders').doc(orderId), {
      'userId': userId,
      'items': items.map((i) => {'productId': i.product.id, 'quantity': i.quantity,
        'price': i.product.price, 'size': i.selectedSize}).toList(),
      'total': items.fold(0.0, (s, i) => s + i.lineTotal),
      'currency': 'USD',
      'status': 'pending',
      'shippingAddress': address.toJson(),
      'createdAt': FieldValue.serverTimestamp(),
    });
  });

  return orderId;
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. 用户模块实现

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Auth Notifier + Firestore User

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:google_sign_in/google_sign_in.dart';

// ⚙️ 依赖:firebase_auth: ^4.0.0, cloud_firestore: ^4.0.0, google_sign_in: ^6.0.0
// User / cartProvider 来自 ShopApp 项目
// User 完整定义见第26课 domain/entities/user.dart

// 简化版 User(完整定义见项目 domain/entities/user.dart)
class User {
  final String uid;
  final String email;
  final String name;
  const User({required this.uid, required this.email, required this.name});

  factory User.fromFirestore(DocumentSnapshot doc) {
    final data = doc.data() as Map<String, dynamic>;
    return User(
      uid: doc.id,
      email: data['email'] ?? '',
      name: data['name'] ?? 'User',
    );
  }
}

@riverpod
class Auth extends _$Auth {
  @override
  AsyncValue<User?> build() {
    FirebaseAuth.instance.authStateChanges().listen((firebaseUser) {
      if (firebaseUser != null) {
        _loadUserProfile(firebaseUser.uid);
      } else {
        state = const AsyncData(null);
      }
    });
    return AsyncData(FirebaseAuth.instance.currentUser != null
      ? User(uid: FirebaseAuth.instance.currentUser!.uid,
             email: FirebaseAuth.instance.currentUser!.email ?? '',
             name: FirebaseAuth.instance.currentUser!.displayName ?? 'User')
      : null);
  }

  Future<void> _loadUserProfile(String uid) async {
    final doc = await FirebaseFirestore.instance.collection('users').doc(uid).get();
    if (doc.exists) {
      state = AsyncData(User.fromFirestore(doc));
    }
  }

  Future<void> loginWithGoogle() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final googleUser = await GoogleSignIn().signIn();
      final googleAuth = await googleUser?.authentication;
      final credential = GoogleAuthProvider.credential(
        accessToken: googleAuth?.accessToken, idToken: googleAuth?.idToken);
      final userCred = await FirebaseAuth.instance.signInWithCredential(credential);
      await _ensureUserProfile(userCred.user!);
      return _mapToUser(userCred.user!);
    });
  }

  Future<void> _ensureUserProfile(User firebaseUser) async {
    final doc = FirebaseFirestore.instance.collection('users').doc(firebaseUser.uid);
    final snapshot = await doc.get();
    if (!snapshot.exists) {
      await doc.set({
        'email': firebaseUser.email,
        'name': firebaseUser.displayName ?? 'User',
        'preferences': {'theme': 'system', 'currency': 'USD', 'locale': 'en'},
        'createdAt': FieldValue.serverTimestamp(),
      });
    }
  }

  Future<void> logout() async {
    await FirebaseAuth.instance.signOut();
    await GoogleSignIn().signOut();
    ref.read(cartProvider.notifier).clear();
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

7. 运营模块实现

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Analytics 事件追踪

DART
import 'package:firebase_analytics/firebase_analytics.dart';

// ⚙️ 依赖:firebase_analytics: ^10.0.0(添加到 dependencies)
// Product 来自 ShopApp 项目,完整定义见第26课

class ShopAnalytics {
  static final _analytics = FirebaseAnalytics.instance;

  static Future<void> logViewProduct(Product p) =>
    _analytics.logEvent(name: 'view_product', parameters: {
      'product_id': p.id, 'product_name': p.name,
      'price': p.price, 'category': p.category, 'currency': p.currency,
    });

  static Future<void> logAddToCart(Product p, int qty) =>
    _analytics.logEvent(name: 'add_to_cart', parameters: {
      'product_id': p.id, 'quantity': qty, 'price': p.price, 'currency': 'USD',
    });

  static Future<void> logBeginCheckout(double total, int items) =>
    _analytics.logEvent(name: 'begin_checkout', parameters: {
      'value': total, 'currency': 'USD', 'item_count': items,
    });

  static Future<void> logPurchase(String orderId, double total) =>
    _analytics.logEvent(name: 'purchase', parameters: {
      'order_id': orderId, 'value': total, 'currency': 'USD',
    });

  static Future<void> logSearch(String query) =>
    _analytics.logEvent(name: 'search', parameters: {'search_term': query});

  static Future<void> setUser(String userId, String name) {
    _analytics.setUserId(id: userId);
    _analytics.setUserProperty(name: 'user_name', value: name);
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:FCM 促销推送处理

DART
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:go_router/go_router.dart';

// ⚙️ 依赖:firebase_messaging: ^14.0.0, go_router: ^13.0.0(添加到 dependencies)
// router 来自 ShopApp 项目 app_router.dart

class PushNotificationService {
  static Future<void> init() async {
    await FirebaseMessaging.instance.requestPermission();
    final token = await FirebaseMessaging.instance.getToken();
    // Send token to server

    FirebaseMessaging.onMessage.listen((message) {
      final title = message.notification?.title ?? 'ShopApp';
      final body = message.notification?.body ?? '';
      // Show local notification or in-app banner
    });

    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      final productId = message.data['product_id'];
      if (productId != null) {
        // Navigate to product detail
        router.push('/product/$productId');
      }
    });
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

8. 完整示例:ShopApp 模块集成 main.dart

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ 依赖:flutter_riverpod: ^2.0.0, firebase_core: ^2.0.0,
//   firebase_analytics: ^10.0.0, shared_preferences: ^2.0.0
// DefaultFirebaseOptions / initHive / PushNotificationService /
// ShopApp / ShopAppTheme / appRouter / AppLocalizations /
// themeModeNotifierProvider / localeNotifierProvider / sharedPreferencesProvider
// 均来自 ShopApp 项目,详见各模块文件

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Core initialization
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  await initHive();
  final prefs = await SharedPreferences.getInstance();

  // Analytics
  await FirebaseAnalytics.instance.logAppOpen();

  // Push notifications
  await PushNotificationService.init();

  runApp(ProviderScope(
    overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
    child: const ShopApp(),
  ));
}

class ShopApp extends ConsumerWidget {
  const ShopApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeNotifierProvider).flutterThemeMode;
    final locale = ref.watch(localeNotifierProvider);

    return MaterialApp.router(
      title: 'ShopApp',
      debugShowCheckedModeBanner: false,
      theme: ShopAppTheme.light(),
      darkTheme: ShopAppTheme.dark(),
      themeMode: themeMode,
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      routerConfig: appRouter,
      builder: (context, child) {
        final mq = MediaQuery.of(context);
        return MediaQuery(
          data: mq.copyWith(textScaleFactor: mq.textScaleFactor.clamp(0.8, 1.5)),
          child: child!,
        );
      },
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q Firestore 事务保证库存不超卖吗?
A 是的。Firestore 事务在读取和写入之间锁定文档,确保并发写入不会导致超卖。
Q Stripe 和原生支付怎么选?
A 跨境/多币种选 Stripe(全球化);单一市场选 Apple Pay/Google Pay(体验更好)。可以同时支持。
Q 购物车数据存本地还是 Firestore?
A 未登录用户存本地(Hive),登录后同步到 Firestore。这样未登录用户也有购物车体验。
Q Firestore 实时监听会消耗大量读配额吗?
A 每个监听连接算 1 次读 + 文档变更时额外读。限制监听范围(where 过滤)和数量(limit 50)控制消耗。
Q 多个 Notifier 之间怎么通信?
A 通过 ref.read 读取其他 Provider。如 Cart Notifier 在 clear() 时调用 Auth Notifier 检查登录状态。避免循环依赖。
Q Analytics 事件量太大会影响性能吗?
A Firebase Analytics 客户端批量上传,不影响 UI 性能。但注意免费配额(每月 1 million 事件)。

📖 小节


📝 作业

  1. 基础题(难度⭐):实现 Product Notifier,支持加载、刷新、分类筛选,用 Hive 做离线缓存。
  2. 进阶题(难度⭐⭐):实现完整的购物车 + 支付流程:CartNotifier + 规格选择 + 模拟支付 + 订单状态机。
  3. 挑战题(难度⭐⭐⭐):实现 ShopApp 全部 5 个模块:商品(Firestore实时) + 购物车(持久化) + 支付(Stripe) + 用户(Auth) + 运营(Analytics+FCM),模块间通过 Riverpod 串联。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏