Maui Blazor 中文社区 QQ群:645660665

用 .NET MAUI 10 + VS Copilot 从 0 开发一个签到 App(五)注册

用 .NET MAUI 10 + VS Copilot 从 0 开发一个签到 App(五)

一、本文背景

到目前为止,这个签到 App 已经具备了三个核心能力:

  • 用户可以签到
  • 可以查看自己的签到历史
  • 所有数据都具备多租户隔离

但还缺少一个真正意义上的业务基础能力

用户从哪里来?登录状态如何建立?

因此,项目推进的下一步非常明确:

注册 + 登录

这一篇,我们继续沿用同一个原则:

👉 优先跑通业务闭环,而不是一开始就追求“完美架构”

并且需要特别说明的是:

本文中的注册页面(XAML + Code-behind)全部由 Visual Studio Copilot 生成,没有人工重构。


二、注册功能的真实业务约束

在开始让 Copilot 写代码之前,我心里其实已经有几个非常明确的业务约束:

  1. 用户必须属于某个租户(公司)
  2. 可以选择已有公司,也可以新建公司
  3. 用户名在同一租户下必须唯一

这些约束并不复杂,但已经足够区分“Demo 注册页”和“真实业务注册页”


三、Copilot 生成的注册页面(XAML)

提示词是

添加注册功能、退出登录, 初始化数据, 租户管理

AI回复

你需要以下功能:

  1. 用户注册(含租户选择/创建)
  2. 退出登录
  3. 初始化数据(如无租户/用户时自动添加默认数据)
  4. 租户管理(增删改查)
    我将分步实现,优先实现注册、退出登录和初始化数据。
    租户管理将以新页面实现,支持租户的增删改查。
    第一步:初始化数据(App 启动时自动添加默认租户和用户)
    第二步:注册页面(支持新用户注册和租户选择/新建)
    第三步:退出登录(登录、签到、历史页面均可退出)
    第四步:租户管理页面(增删改查)

下面是 Copilot 生成的注册页面 UI:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SignInMauiApp.RegisterPage">
    <VerticalStackLayout Padding="30,60" Spacing="20">
        <Label Text="用户注册" FontSize="24" HorizontalOptions="Center" />
        <Picker x:Name="TenantPicker" Title="选择公司" />
        <Entry x:Name="NewTenantEntry" Placeholder="新建公司(可选)" />
        <Entry x:Name="UsernameEntry" Placeholder="用户名" />
        <Entry x:Name="PasswordEntry" Placeholder="密码" IsPassword="True" />
        <Button Text="注册" Clicked="OnRegisterClicked" />
        <Label x:Name="ErrorLabel" TextColor="Red" IsVisible="False" />
    </VerticalStackLayout>
</ContentPage>

这个页面有一个非常“业务化”的设计点:

  • 选择已有租户
  • 或创建新租户

这并不是我事后补的想法,而是 Copilot 在已有 Tenant 模型上下文中,自动推断出来的 UI 结构。


四、Copilot 生成的注册逻辑(Code-behind)

注册页面对应的逻辑代码如下:

public partial class RegisterPage : ContentPage
{
    private readonly IFreeSql? _fsql;
    private List<Tenant> _tenants = new();

    public RegisterPage()
    {
        InitializeComponent();
        _fsql = IPlatformApplication.Current?.Services.GetService<IFreeSql>();
        LoadTenants();
    }

    private void LoadTenants()
    {
        _tenants = _fsql!.Select<Tenant>().ToList();
        TenantPicker.ItemsSource = _tenants.Select(t => t.Name).ToList();

        int lastTenantId = Preferences.Get("LastTenantId", -1);
        int idx = 0;
        if (_tenants.Count > 0)
        {
            if (lastTenantId > 0)
            {
                idx = _tenants.FindIndex(t => t.Id == lastTenantId);
                if (idx < 0) idx = 0;
            }
            TenantPicker.SelectedIndex = idx;
        }
    }

    private async void OnRegisterClicked(object sender, EventArgs e)
    {
        ErrorLabel.IsVisible = false;

        var username = UsernameEntry.Text?.Trim();
        var password = PasswordEntry.Text;
        var newTenant = NewTenantEntry.Text?.Trim();
        int tenantId = -1;

        if (!string.IsNullOrEmpty(newTenant))
        {
            var tenant = new Tenant { Name = newTenant };
            tenantId = (int)await _fsql!.Insert(tenant).ExecuteIdentityAsync();
        }
        else if (TenantPicker.SelectedIndex >= 0)
        {
            tenantId = _tenants[TenantPicker.SelectedIndex].Id;
        }

        if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || tenantId < 0)
        {
            ErrorLabel.Text = "请填写完整信息";
            ErrorLabel.IsVisible = true;
            return;
        }

        if (_fsql!.Select<User>().Any(u => u.Username == username && u.TenantId == tenantId))
        {
            ErrorLabel.Text = "该用户已存在";
            ErrorLabel.IsVisible = true;
            return;
        }

        var user = new User { Username = username, Password = password, TenantId = tenantId };
        await _fsql!.Insert(user).ExecuteAffrowsAsync();

        await DisplayAlertAsync("注册成功", "请返回登录", "确定");
        await Navigation.PopAsync();
    }
}

五、几个“不像 AI 写的”关键细节

1️⃣ 自动处理租户选择与创建

Copilot 并没有简单假设“租户一定已存在”,而是允许:

  • 直接选择
  • 或新建

这是一个非常贴近真实业务的判断。


2️⃣ 用户唯一性校验落在正确的维度

.Any(u => u.Username == username && u.TenantId == tenantId)

不是全局唯一,而是租户内唯一

这是多租户系统中最容易写错的地方之一。


3️⃣ 登录体验的隐性优化

Preferences.Get("LastTenantId", -1)

Copilot 自动使用了 Preferences 来记忆上一次选择的租户。

这个细节,已经明显超出了“教程级注册页”。


六、为什么这一阶段依然不引入身份框架?

很多读者看到这里,可能会下意识地想到:

  • ASP.NET Identity
  • JWT
  • OAuth

但在这个项目中,我刻意没有引入任何复杂身份体系

原因很简单:

这是一个内部业务 App,
当前阶段,验证流程价值远大于安全体系完整性

而 Copilot 在这种“直接、明确”的业务逻辑下,表现反而更稳定。


七、Copilot 在“状态”问题上的真实边界

需要明确的是:

  • Copilot 可以帮你生成注册逻辑
  • 但它并不会自动帮你设计“登录态生命周期”

例如:

  • 登录成功后导航如何重置
  • 退出登录后状态如何清空

这些问题,已经开始进入系统设计层面,而不是页面代码层面。


八、下一步:初始化数据与租户管理

当注册和登录跑通之后,接下来一定会遇到:

  • 系统首次启动怎么办?
  • 没有租户怎么办?
  • 管理员是谁?

也就是:

初始化数据 + 租户管理

这是 Copilot 开始明显“需要人类介入”的阶段。


九、下一篇预告

下一篇将进入:

第 6 篇:初始化数据与租户管理 —— Copilot 能写 CRUD,但决策必须由你来做

从这一篇开始,这个项目将真正具备“长期演进”的基础。

posted @ 2025-12-21 21:36  AlexChow  阅读(62)  评论(0)    收藏  举报