Loading...
正在加载...
请稍候

第十二章:状态管理与数据持久化策略

✨步子哥 (steper) 2026年02月17日 05:28

第十二章:状态管理与数据持久化策略

本章导读:想象你正在阅读一本精彩的小说,读到一半时,电话铃响了,你不得不放下书本去接电话。当你回来继续阅读时,你希望能够准确地从刚才停下的地方继续——而不是从第一页重新开始。应用的状态管理就是这样一个"书签"系统。在移动设备和 Web 浏览器中,系统随时可能因为内存不足而终止你的应用后台。如何在被"杀掉"后优雅地恢复?如何让用户感觉应用从未离开过?本章将揭示这些问题的答案,带你构建真正具有"记忆"的应用。


🔄 12.1 应用生命周期:理解"幸存"的艺术

在深入技术细节之前,我们需要先理解移动应用与桌面应用的根本差异。在传统的 Windows 桌面应用中,用户决定何时关闭应用——应用可以一直运行,直到用户点击关闭按钮。但在移动世界和 Web 世界中,这个主动权被操作系统夺走了。

当用户按下 Home 键切换到另一个应用时,你的应用并不会立即终止,而是进入"后台"状态。如果系统内存充足,它可能会在后台保持一段时间。但当用户打开更多应用、系统内存紧张时,操作系统会毫不留情地终止你的应用进程——而且不会给你任何警告。这就是为什么移动应用必须具备"断点续传"的能力。

第一性原理:为什么移动操作系统要如此"残忍"地杀掉后台应用?答案在于资源约束。移动设备的内存和电池都是有限的。如果所有应用都在后台持续运行,设备很快就会变得缓慢甚至死机,电池也会在几小时内耗尽。操作系统必须做出取舍:优先保证前台应用的流畅体验,牺牲后台应用的存活。这是一种"残酷但必要"的资源调度策略。

📊 12.1.1 生命周期状态图

Uno Platform 映射了 WinUI 的生命周期事件,让开发者能够在关键时刻保存和恢复状态。让我们通过一个状态图来理解应用的整个生命周期。

┌─────────────┐
│  NotRunning │  应用尚未启动或已被终止
└──────┬──────┘
       │ 用户点击应用图标
       ▼
┌─────────────┐
│  Running    │  应用在前台运行,与用户交互
└──────┬──────┘
       │ 用户按 Home 键或切换到其他应用
       ▼
┌─────────────┐
│ Suspending  │  应用即将进入后台(约5秒窗口)
└──────┬──────┘
       │ 保存状态完成
       ▼
┌─────────────┐
│ Suspended   │  应用在后台,进程可能仍存在
└──────┬──────┘
       │ 用户返回应用 或 系统终止进程
       ▼
┌─────────────┐
│ Resuming    │  应用从后台恢复(如果进程仍存在)
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  Running    │  继续运行
└─────────────┘

🎯 12.1.2 处理生命周期事件

// 文件位置:App.xaml.cs
using Microsoft.UI.Xaml;
using Windows.ApplicationModel.Activation;
using Windows.Storage;

public sealed partial class App : Application
{
    /// <summary>
    /// 应用级别的状态容器
    /// 用于在挂起和恢复之间保存临时数据
    /// </summary>
    private readonly AppState _appState = new AppState();

    public App()
    {
        this.InitializeComponent();

        // 订阅应用生命周期事件
        // Suspending: 应用即将进入后台时的最后通知
        // 通常你有约 5 秒的时间完成保存操作
        this.Suspending += OnSuspending;

        // Resuming: 应用从后台恢复
        // 注意:只有当进程没有被终止时才会触发
        // 如果进程被终止后重新启动,会走正常的启动流程
        this.Resuming += OnResuming;
    }

    /// <summary>
    /// 应用即将挂起时的处理
    /// 这是保存临时状态的最后机会
    /// </summary>
    private async void OnSuspending(object sender, Windows.ApplicationModel.SuspendingEventArgs e)
    {
        // 获取一个 Deferral,表示我们需要异步操作
        // 在 Deferral 完成之前,系统不会挂起应用
        var deferral = e.SuspendingOperation.GetDeferral();

        try
        {
            // 保存应用状态到本地存储
            // 包括:当前页面、用户输入的表单数据、滚动位置等
            await SaveAppStateAsync();

            // 如果使用了 SQLite 数据库,确保所有待写入的数据已刷新到磁盘
            await DatabaseService.FlushAsync();

            // 记录挂起时间,用于恢复时判断数据是否过期
            var settings = ApplicationData.Current.LocalSettings;
            settings.Values["LastSuspendTime"] = DateTime.Now.ToString("o");
        }
        catch (Exception ex)
        {
            // 即使保存失败,也要记录日志
            // 但不要抛出异常,否则可能导致应用崩溃
            System.Diagnostics.Debug.WriteLine({{LATEX:0}}"加载状态失败: {ex.Message}");
            return null;
        }
    }

    protected override async void OnLaunched(LaunchActivatedEventArgs args)
    {
        // 检查是否有之前保存的状态
        var savedState = await LoadAppStateAsync();

        if (savedState != null)
        {
            // 从保存的状态恢复
            // 注意:需要验证状态的时效性
            var stateAge = DateTime.Now - savedState.Timestamp;
            if (stateAge.TotalHours < 24) // 状态在24小时内有效
            {
                RestoreFromState(savedState);
            }
        }

        // 正常的窗口创建和导航
        var window = new Window();
        // ... 导航到初始页面
    }
}

/// <summary>
/// 应用状态数据结构
/// </summary>
public class AppStateData
{
    public string CurrentPage { get; set; }
    public List<string> NavigationStack { get; set; }
    public Dictionary<string, string> FormData { get; set; }
    public Dictionary<string, double> ScrollPositions { get; set; }
    public DateTime Timestamp { get; set; }
}

费曼技巧提问:为什么 Suspending 事件处理器需要使用 Deferral?想象你在餐厅用餐,突然接到电话需要离开。你告诉服务员"请稍等,我需要打包"。服务员会等你打包完成,而不是立即收走你的盘子。Deferral 就是那个"请稍等"的信号——它告诉操作系统"我还有事情要做,请等我完成再挂起"。


⚙️ 12.2 轻量级配置:ApplicationData 应用数据容器

对于简单的配置信息——比如用户的主题偏好、通知设置、首次运行标识——你不需要数据库,也不需要复杂的序列化。Uno Platform 提供了极其简单的键值对存储机制:ApplicationData

📦 12.2.1 本地设置(LocalSettings)

LocalSettings 是一个轻量级的字典存储,数据保存在设备本地,不会跨设备同步。它适合存储设备相关的偏好设置。

using Windows.Storage;

public class SettingsService
{
    // 获取本地设置容器
    // 这是一个持久化的键值对存储
    private ApplicationDataContainer LocalSettings => ApplicationData.Current.LocalSettings;

    /// <summary>
    /// 用户主题偏好
    /// </summary>
    public ElementTheme ThemePreference
    {
        get
        {
            // 尝试从设置中读取
            // ?? 右边的值是默认值(当设置不存在时使用)
            if (LocalSettings.Values["ThemePreference"] is string themeStr)
            {
                return Enum.Parse<ElementTheme>(themeStr);
            }
            return ElementTheme.Default;
        }
        set
        {
            // 保存到设置
            // 枚举值存储为字符串,便于调试和版本兼容
            LocalSettings.Values["ThemePreference"] = value.ToString();
        }
    }

    /// <summary>
    /// 是否首次运行
    /// </summary>
    public bool IsFirstRun
    {
        get => (bool)(LocalSettings.Values["IsFirstRun"] ?? true);
        set => LocalSettings.Values["IsFirstRun"] = value;
    }

    /// <summary>
    /// 上次同步时间
    /// </summary>
    public DateTime? LastSyncTime
    {
        get
        {
            if (LocalSettings.Values["LastSyncTime"] is string timeStr)
            {
                return DateTime.Parse(timeStr);
            }
            return null;
        }
        set
        {
            if (value.HasValue)
            {
                // 使用 ISO 8601 格式存储日期时间
                // "o" 格式保证时区信息的完整性
                LocalSettings.Values["LastSyncTime"] = value.Value.ToString("o");
            }
            else
            {
                LocalSettings.Values.Remove("LastSyncTime");
            }
        }
    }

    /// <summary>
    /// 字体大小偏好
    /// </summary>
    public int FontSize
    {
        get => (int)(LocalSettings.Values["FontSize"] ?? 14);
        set => LocalSettings.Values["FontSize"] = value;
    }

    /// <summary>
    /// 复合设置:用户档案
    /// 展示如何存储更复杂的数据
    /// </summary>
    public UserProfile UserProfile
    {
        get
        {
            if (LocalSettings.Values["UserProfile"] is string json)
            {
                return JsonSerializer.Deserialize<UserProfile>(json);
            }
            return null;
        }
        set
        {
            if (value != null)
            {
                string json = JsonSerializer.Serialize(value);
                LocalSettings.Values["UserProfile"] = json;
            }
            else
            {
                LocalSettings.Values.Remove("UserProfile");
            }
        }
    }

    /// <summary>
    /// 清除所有设置(用于退出登录)
    /// </summary>
    public void ClearAll()
    {
        LocalSettings.Values.Clear();
    }
}

public enum ElementTheme
{
    Default,
    Light,
    Dark
}

public class UserProfile
{
    public string UserId { get; set; }
    public string DisplayName { get; set; }
    public string Email { get; set; }
}

☁️ 12.2.2 漫游设置(RoamingSettings)

漫游设置是一种特殊的设置容器,它会在用户的所有设备之间自动同步。在 Windows 平台上,这是通过微软账户实现的。在 Android 和 iOS 上,Uno Platform 目前将其映射为本地存储,但未来的版本可能会集成 iCloud 和 Google Drive 同步。

public class RoamingSettingsService
{
    // 获取漫游设置容器
    private ApplicationDataContainer RoamingSettings => ApplicationData.Current.RoamingSettings;

    /// <summary>
    /// 同步用户的阅读偏好
    /// 这些偏好会在用户的所有设备上同步
    /// </summary>
    public bool AutoPlayVideos
    {
        get => (bool)(RoamingSettings.Values["AutoPlayVideos"] ?? true);
        set => RoamingSettings.Values["AutoPlayVideos"] = value;
    }

    /// <summary>
    /// 监听漫游设置变化
    /// 当设置从其他设备同步过来时,会触发此事件
    /// </summary>
    public void Initialize()
    {
        ApplicationData.Current.DataChanged += OnDataChanged;
    }

    private void OnDataChanged(ApplicationData sender, object args)
    {
        // 漫游数据已更新
        // 在这里刷新 UI 或通知相关组件
        System.Diagnostics.Debug.WriteLine("漫游设置已同步");
    }
}

技术术语Roaming(漫游) 在计算机领域指的是数据随用户移动的能力。就像你的手机漫游到国外时仍然可以使用一样,漫游设置让你的应用配置在用户的任何设备上都保持一致。想象你在办公室的电脑上设置了深色主题,回到家后打开平板,主题设置自动同步——这就是漫游的魅力。


🗄️ 12.3 跨平台数据库:SQLite 深度实战

当应用需要处理成千上万条结构化数据时,键值对存储就显得力不从心了。这时,你需要一个真正的数据库。在跨平台开发领域,SQLite 是无可争议的王者——它轻量、快速、零配置,而且完全开源。

📦 12.3.1 为什么选择 SQLite?

SQLite 的设计哲学可以用一句话概括:"零配置、无服务器、自包含"。与 MySQL 或 PostgreSQL 不同,SQLite 不需要一个单独的服务器进程。整个数据库就是一个普通的文件,存储在你的应用沙盒中。这意味着:

数据库文件随着应用卸载而自动清理,不会留下残留。读写操作直接在文件系统层面进行,没有网络开销。跨平台兼容性极佳——同一个数据库文件可以在 Android、iOS、Windows 和 WASM 上读写。

第一性原理:为什么 SQLite 能够成为跨平台数据库的标准?因为它的核心是一个纯 C 语言实现的库,几乎可以在任何有 C 编译器的平台上运行。而且 SQLite 团队对向后兼容性的承诺极其坚定——"永远不破坏兼容性"是他们的核心原则。这意味着你可以放心地使用 SQLite,不用担心未来的版本会导致你的应用崩溃。

🔧 12.3.2 安装与配置

在开始使用 SQLite 之前,你需要安装几个 NuGet 包。打开你的 Shared 项目,添加以下依赖:

<!-- 在 Shared 项目的 .csproj 文件中 -->
<ItemGroup>
    <!-- SQLite ORM 库 -->
    <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />

    <!-- SQLite 原生库(提供各平台的 SQLite 实现) -->
    <PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.4" />
</ItemGroup>

💻 12.3.3 数据库服务实现

让我们创建一个完整的数据库服务,展示 SQLite 在 Uno Platform 中的最佳实践:

// 文件位置:Services/DatabaseService.cs
using SQLite;
using Windows.Storage;

public class DatabaseService
{
    private SQLiteAsyncConnection _database;
    private readonly string _databaseFileName = "MyAppData.db3";

    /// <summary>
    /// 初始化数据库
    /// 应该在应用启动时调用
    /// </summary>
    public async Task InitializeAsync()
    {
        if (_database != null)
            return;

        // 获取数据库文件路径
        // ApplicationData.Current.LocalFolder 在各平台上都指向应用的私有存储目录
        string dbPath = Path.Combine(
            ApplicationData.Current.LocalFolder.Path,
            _databaseFileName
        );

        // 创建数据库连接
        // SQLiteAsyncConnection 提供异步 API,不会阻塞 UI 线程
        _database = new SQLiteAsyncConnection(dbPath);

        // 创建所有表(如果不存在)
        // CreateTableAsync 是幂等的——重复调用不会有副作用
        await _database.CreateTableAsync<TodoItem>();
        await _database.CreateTableAsync<Category>();
        await _database.CreateTableAsync<Tag>();

        System.Diagnostics.Debug.WriteLine({{LATEX:1}}"凭据已保存: {resource}/{userName}");
    }

    /// <summary>
    /// 获取凭据
    /// </summary>
    public PasswordCredential GetCredential(string resource, string userName)
    {
        try
        {
            // 从安全存储中检索凭据
            var credential = _vault.Retrieve(resource, userName);

            // 需要调用 RetrievePassword 才能获取实际的密码值
            credential.RetrievePassword();

            return credential;
        }
        catch (Exception ex)
        {
            // 凭据不存在或访问失败
            System.Diagnostics.Debug.WriteLine({{LATEX:2}}"删除凭据失败: {ex.Message}");
        }
    }

    /// <summary>
    /// 检查凭据是否存在
    /// </summary>
    public bool HasCredential(string resource, string userName)
    {
        try
        {
            _vault.Retrieve(resource, userName);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

💡 12.4.2 实际应用示例:OAuth 令牌存储

// 文件位置:Services/AuthService.cs
public class AuthService
{
    private const string TOKEN_RESOURCE = "MyApp_OAuth";
    private const string ACCESS_TOKEN_KEY = "access_token";
    private const string REFRESH_TOKEN_KEY = "refresh_token";

    private readonly SecureStorageService _secureStorage;

    public AuthService()
    {
        _secureStorage = new SecureStorageService();
    }

    /// <summary>
    /// 保存认证令牌
    /// </summary>
    public void SaveTokens(string accessToken, string refreshToken)
    {
        // 分别保存访问令牌和刷新令牌
        _secureStorage.SaveCredential(TOKEN_RESOURCE, ACCESS_TOKEN_KEY, accessToken);
        _secureStorage.SaveCredential(TOKEN_RESOURCE, REFRESH_TOKEN_KEY, refreshToken);
    }

    /// <summary>
    /// 获取访问令牌
    /// </summary>
    public string GetAccessToken()
    {
        var credential = _secureStorage.GetCredential(TOKEN_RESOURCE, ACCESS_TOKEN_KEY);
        return credential?.Password;
    }

    /// <summary>
    /// 获取刷新令牌
    /// </summary>
    public string GetRefreshToken()
    {
        var credential = _secureStorage.GetCredential(TOKEN_RESOURCE, REFRESH_TOKEN_KEY);
        return credential?.Password;
    }

    /// <summary>
    /// 检查用户是否已登录
    /// </summary>
    public bool IsLoggedIn()
    {
        return _secureStorage.HasCredential(TOKEN_RESOURCE, ACCESS_TOKEN_KEY);
    }

    /// <summary>
    /// 登出:清除所有令牌
    /// </summary>
    public void Logout()
    {
        _secureStorage.RemoveCredential(TOKEN_RESOURCE, ACCESS_TOKEN_KEY);
        _secureStorage.RemoveCredential(TOKEN_RESOURCE, REFRESH_TOKEN_KEY);
    }
}

技术术语Keychain(iOS)和 KeyStore(Android)是操作系统级别的安全存储系统。它们使用硬件支持的加密模块(如 iPhone 的 Secure Enclave)来保护敏感数据。即使攻击者获得了设备的 root 权限,也很难从这些安全存储中提取明文数据。这就是为什么你应该始终使用 PasswordVault 而不是自己实现加密。


📡 12.5 现代缓存策略:离线优先架构

如果你正在构建一个严重依赖网络 API 的应用,你需要考虑离线场景。用户可能在地铁里、飞机上,或者只是网络连接不稳定。一个优秀的应用应该能够在离线时继续工作,并在联网后自动同步。

🔄 12.5.1 缓存层次结构

一个成熟的缓存系统通常包含多个层次,每一层都有不同的特点和用途。

┌─────────────────────────────────────────────┐
│               Memory Cache                  │  速度:最快
│          (内存中的对象缓存)                   │  持久性:应用生命周期
└─────────────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────┐
│               Disk Cache                    │  速度:快
│        (本地文件或 SQLite 缓存)              │  持久性:应用安装期间
└─────────────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────┐
│               Network                       │  速度:取决于网络
│           (远程 API 请求)                    │  持久性:无(实时数据)
└─────────────────────────────────────────────┘

📦 12.5.2 实现离线优先的数据服务

// 文件位置:Services/CachedDataService.cs
using Windows.Storage;

public class CachedDataService
{
    private readonly HttpClient _httpClient;
    private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1, 1);

    // 内存缓存
    private readonly Dictionary<string, (object Data, DateTime Expiry)> _memoryCache = new();

    /// <summary>
    /// 获取数据(优先从缓存读取)
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <param name="cacheKey">缓存键</param>
    /// <param name="apiUrl">API 地址</param>
    /// <param name="cacheDuration">缓存有效期</param>
    /// <param name="forceRefresh">是否强制刷新</param>
    public async Task<T> GetDataAsync<T>(
        string cacheKey,
        string apiUrl,
        TimeSpan cacheDuration,
        bool forceRefresh = false) where T : class
    {
        // 1. 检查内存缓存
        if (!forceRefresh && TryGetFromMemoryCache<T>(cacheKey, out var memoryData))
        {
            System.Diagnostics.Debug.WriteLine({{LATEX:3}}"从磁盘缓存获取: {cacheKey}");
            return diskData;
        }

        // 3. 从网络获取
        try
        {
            var networkData = await FetchFromNetworkAsync<T>(apiUrl);

            // 更新所有缓存层
            SetMemoryCache(cacheKey, networkData, cacheDuration);
            await SaveToDiskCacheAsync(cacheKey, networkData);

            System.Diagnostics.Debug.WriteLine({{LATEX:4}}"网络请求失败: {ex.Message}");

            // 如果有过期的磁盘缓存,即使过期也返回(离线模式)
            if (await TryGetFromDiskCacheAsync<T>(cacheKey, out var staleData, ignoreExpiry: true))
            {
                System.Diagnostics.Debug.WriteLine({{LATEX:5}}"{key}.json") as StorageFile;

            if (cacheFile == null)
            {
                data = null;
                return false;
            }

            string json = await FileIO.ReadTextAsync(cacheFile);
            var cacheEntry = JsonSerializer.Deserialize<CacheEntry<T>>(json);

            if (!ignoreExpiry && DateTime.Now > cacheEntry.Expiry)
            {
                await cacheFile.DeleteAsync(); // 清除过期文件
                data = null;
                return false;
            }

            data = cacheEntry.Data;
            return data != null;
        }
        catch
        {
            data = null;
            return false;
        }
        finally
        {
            _cacheLock.Release();
        }
    }

    private async Task SaveToDiskCacheAsync<T>(string key, T data)
    {
        await _cacheLock.WaitAsync();
        try
        {
            StorageFolder cacheFolder = await GetCacheFolderAsync();
            StorageFile cacheFile = await cacheFolder.CreateFileAsync(
                $"{key}.json",
                CreationCollisionOption.ReplaceExisting
            );

            var cacheEntry = new CacheEntry<T>
            {
                Data = data,
                CachedAt = DateTime.Now,
                Expiry = DateTime.Now.AddDays(7) // 磁盘缓存保留7天
            };

            string json = JsonSerializer.Serialize(cacheEntry);
            await FileIO.WriteTextAsync(cacheFile, json);
        }
        finally
        {
            _cacheLock.Release();
        }
    }

    private async Task<T> FetchFromNetworkAsync<T>(string url)
    {
        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();

        string json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(json);
    }

    private async Task<StorageFolder> GetCacheFolderAsync()
    {
        StorageFolder localFolder = ApplicationData.Current.LocalFolder;
        return await localFolder.CreateFolderAsync("cache", CreationCollisionOption.OpenIfExists);
    }

    /// <summary>
    /// 清除所有缓存
    /// </summary>
    public async Task ClearCacheAsync()
    {
        _memoryCache.Clear();

        StorageFolder cacheFolder = await GetCacheFolderAsync();
        await cacheFolder.DeleteAsync();
    }
}

/// <summary>
/// 缓存条目结构
/// </summary>
public class CacheEntry<T>
{
    public T Data { get; set; }
    public DateTime CachedAt { get; set; }
    public DateTime Expiry { get; set; }
}

📝 本章小结

状态管理是应用的"记忆系统",决定了用户在应用切换、设备重启、网络中断等场景下的体验。通过本章的学习,你已经掌握了构建健壮应用状态管理系统的核心技能。

让我们回顾本章的关键要点:

第一,理解应用生命周期是状态管理的基础。在移动平台和 Web 上,应用随时可能被系统终止。你必须在 Suspending 事件中保存状态,并在恢复时正确还原。

第二,ApplicationData 提供了简单高效的键值存储。对于配置信息和用户偏好这类轻量级数据,它是最佳选择。漫游设置还能实现跨设备同步。

第三,SQLite 是跨平台结构化数据存储的标准方案。通过 ORM 库如 sqlite-net-pcl,你可以用 C# 对象来操作数据库,无需手写 SQL。

第四,敏感数据必须使用 PasswordVault 安全存储。它利用各平台的原生安全设施,为密码、令牌等敏感信息提供硬件级别的保护。

第五,离线优先架构通过多级缓存策略实现。内存缓存提供最快访问,磁盘缓存确保持久性,网络获取最新数据。在网络不可用时,过期缓存也能作为最后的备选方案。

在下一章中,我们将进入企业级开发的关键领域——身份验证与安全。你将学习如何在 Uno Platform 中实现安全的登录流程,集成 OAuth2 认证,以及保护 API 通信。


动手实验

  1. 生命周期感知应用:创建一个显示生命周期状态的应用。在屏幕上实时显示应用当前处于 Running、Suspended 还是 Resuming 状态。使用 Suspending 事件保存当前时间,在恢复时显示应用被挂起了多长时间。
  2. 待办事项管理器:使用 SQLite 实现一个完整的待办事项应用。支持添加、编辑、删除、标记完成功能。按优先级和截止日期排序显示。确保应用在被终止后能够恢复所有数据。
  3. 离线新闻阅读器:实现一个简单的新闻阅读器,支持离线阅读。首次加载时从网络获取数据并缓存到本地。后续启动时优先显示缓存数据,同时在后台检查更新。在网络不可用时,显示"离线模式"提示。

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录