更新代码

This commit is contained in:
chfei committed 2026-11-13 19:19:33 +08:00
1 parent 127fc22de5
commit 45829fa267
26 files changed
+139 -90

No files matched your search

+4 -9
View File
@@ -359,16 +359,11 @@ if (AppFileHelper.PickFolder(out string path)) { FolderPath = path; }
XAML 细则见 `wpf-xaml.mdc`。
## 数据映射(忽略)
## 服务端实体与 WPF 展示模型
使用 Mapster:
```csharp
using PipeReviewReport.Shared;
var dto = model.Adapt<XxxDto>();
var model = dto.Adapt<XxxModel>();
```
- 服务端返回的业务实体命名为 `XxxInfo`(如 `UserInfo`),放在独立的 `Store.Management.Models` 项目中,可脱离 API 和 WPF 使用。
- WPF 视图绑定展示模型。单个实体的展示模型命名为 `XxxViewModel`(如 `UserViewModel`),集合中的每个元素命名为 `XxxItemViewModel`(如 `IReadOnlyList<UserItemViewModel>` 中的 `UserItemViewModel`)。
- 从服务端实体到展示模型的转换使用 Riok.Mapperly,映射类放在使用方项目的 `Mappers/` 目录。
## 资源引用(忽略)
+1 -1
View File
@@ -28,7 +28,7 @@ disable-model-invocation: false
| 文件 | 加载策略 | 内容范围 |
|---|---|---|
| `.agents/rules/wpf-core.md` | alwaysApply: true | 项目结构、命名约定、ViewModel、MVVM属性、Command、服务、DI、对话框、LoadingStatus、文件对话框/文件夹选择、输入限长、Mapster |
| `.agents/rules/wpf-core.md` | alwaysApply: true | 项目结构、命名约定、ViewModel、MVVM属性、Command、服务、DI、对话框、LoadingStatus、文件对话框/文件夹选择、输入限长、实体与展示模型映射 |
| `.agents/rules/csharp-syntax.md` | alwaysApply: true | C# 语法硬约束(禁止 var、null检查、Debug.WriteLine、DRY等) |
| `.agents/rules/architecture.md` | alwaysApply: true | 技术栈、项目架构、编译校验 |
| `.agents/rules/editor-conventions.md` | alwaysApply: true | 换行符、编码规范 |
@@ -1,3 +1,4 @@
using Store.Management.Models;
using System.ComponentModel.DataAnnotations;
using RestSharp;
using Store.Management.Api.Transport;
@@ -13,15 +14,6 @@ public sealed class LoginRequest
[StringLength(100)] public string? DeviceName { get; set; }
}
public sealed class LoginResult
{
public string TokenType { get; init; } = "Bearer";
public string Token { get; init; } = "";
public uint ExpiresInMinutes { get; init; }
}
public sealed record CurrentUser(string UserName, string? NickName);
public sealed class AuthApi(ApiTransport transport, TokenSession session)
{
// 登录和退出串行执行,避免迟到的登录响应覆盖退出操作。
@@ -7,7 +7,6 @@ public sealed class ApiResult<T>
public string? TraceId { get; init; }
public T? Data { get; init; }
}
public sealed record PageResult<T>(long Total, IReadOnlyList<T> Items);
public class PageRequest
@@ -4,4 +4,7 @@
<ItemGroup>
<PackageReference Include="RestSharp" Version="112.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Store.Management.Models\Store.Management.Models.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
using RestSharp;
using Store.Management.Api.Models;
using Store.Management.Api.Transport;
using Store.Management.Models;
namespace Store.Management.Api.Users;
@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Store.Management.Api.Users
{
public enum UserGroup : byte
{
/// <summary>主号。</summary>
[Description("主号")] MainAccount = 3,
/// <summary>客服。</summary>
[Description("客服")] CustomerService = 4,
/// <summary>加盟(旧系统分组 5)。</summary>
}
}
@@ -1,3 +1,4 @@
using Store.Management.Models;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Store.Management.Api.Models;
@@ -32,12 +33,6 @@ public sealed class UserCreateRequest : UserEditRequest
public sealed record ChangePasswordRequest(string UserName, string NewPassword, string? OldPassword = null);
public enum UserAdminLevel : short
{
[System.ComponentModel.Description("普通用户")] Normal = 0,
[System.ComponentModel.Description("超级管理员")] SuperAdmin = 1
}
public sealed class UserUpdateRequest : UserEditRequest
{
[Range(1, int.MaxValue)] public int Id { get; set; }
@@ -57,28 +52,3 @@ public sealed class UserBillingUpdateRequest
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public decimal? SyncLogisticsPerOrder { get; set; }
}
/// <summary>管理端当前使用的用户资料字段;不接收密码。</summary>
public sealed class UserInfo
{
public int Id { get; init; }
public string UserName { get; init; } = "";
public string? NickName { get; init; }
public string? Mobile { get; init; }
public string? CompanyName { get; init; }
public string? Notes { get; init; }
public string? ParentUserName { get; init; }
public int GroupId { get; init; }
[System.Text.Json.Serialization.JsonIgnore]
public UserGroup? Group => GroupId is >= byte.MinValue and <= byte.MaxValue ? (UserGroup)GroupId : null;
public UserAdminLevel? SaLevel { get; init; }
public string? OemId { get; init; }
public string? Authority { get; init; }
public int ShopPointsPerMonth { get; init; }
public decimal? PostagePerOrder { get; init; }
public decimal? RefundPostagePerOrder { get; init; }
public decimal? GiftOemAmountPerOrder { get; init; }
public decimal? SyncLogisticsPerOrder { get; init; }
public decimal Points { get; init; }
public decimal? Balance { get; init; }
}
@@ -0,0 +1,12 @@
using Riok.Mapperly.Abstractions;
using Store.Management.Framework.ViewModels;
using Store.Management.Models;
namespace Store.Management.Framework.Mappers;
[Mapper]
public static partial class UserMapper
{
public static partial UserItemViewModel ToItemViewModel(UserInfo user);
public static partial IReadOnlyList<UserItemViewModel> ToItemViewModels(IReadOnlyList<UserInfo> users);
}
@@ -1,7 +1,8 @@
using Store.Management.Models;
using MAX.Core.DependencyInjection;
using Store.Management.Api;
using Store.Management.Api.Models;
using Store.Management.Api.Users;
using Store.Management.Api.Models;
namespace Store.Management.Framework.Services;
@@ -14,6 +14,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Store.Management.Models\Store.Management.Models.csproj" />
<PackageReference Include="Riok.Mapperly" Version="4.3.1" PrivateAssets="all" />
<PackageReference Include="HandyControl" Version="3.6.2.1" />
<PackageReference Include="MAX.Common.WPF" Version="1.4.23" />
<ProjectReference Include="..\Store.Management.Api\Store.Management.Api.csproj" />
@@ -0,0 +1,26 @@
using Store.Management.Models;
namespace Store.Management.Framework.ViewModels;
public sealed class UserItemViewModel
{
public int Id { get; init; }
public string UserName { get; init; } = "";
public string? NickName { get; init; }
public string? Mobile { get; init; }
public string? CompanyName { get; init; }
public string? Notes { get; init; }
public string? ParentUserName { get; init; }
public int GroupId { get; init; }
public UserGroup? Group => GroupId is >= byte.MinValue and <= byte.MaxValue ? (UserGroup)GroupId : null;
public UserAdminLevel? SaLevel { get; init; }
public string? OemId { get; init; }
public string? Authority { get; init; }
public int ShopPointsPerMonth { get; init; }
public decimal? PostagePerOrder { get; init; }
public decimal? RefundPostagePerOrder { get; init; }
public decimal? GiftOemAmountPerOrder { get; init; }
public decimal? SyncLogisticsPerOrder { get; init; }
public decimal Points { get; init; }
public decimal? Balance { get; init; }
}
@@ -0,0 +1,3 @@
namespace Store.Management.Models;
public sealed record CurrentUser(string UserName, string? NickName);
@@ -0,0 +1,8 @@
namespace Store.Management.Models;
public sealed class LoginResult
{
public string TokenType { get; init; } = "Bearer";
public string Token { get; init; } = "";
public uint ExpiresInMinutes { get; init; }
}
@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>
@@ -0,0 +1,7 @@
namespace Store.Management.Models;
public enum UserAdminLevel : short
{
[System.ComponentModel.Description("普通用户")] Normal = 0,
[System.ComponentModel.Description("超级管理员")] SuperAdmin = 1
}
@@ -0,0 +1,9 @@
namespace Store.Management.Models;
public enum UserGroup : byte
{
/// <summary>主号。</summary>
[System.ComponentModel.Description("主号")] MainAccount = 3,
/// <summary>客服。</summary>
[System.ComponentModel.Description("客服")] CustomerService = 4,
}
@@ -0,0 +1,24 @@
namespace Store.Management.Models;
/// <summary>管理端当前使用的用户资料字段;不接收密码。</summary>
public sealed class UserInfo
{
public int Id { get; init; }
public string UserName { get; init; } = "";
public string? NickName { get; init; }
public string? Mobile { get; init; }
public string? CompanyName { get; init; }
public string? Notes { get; init; }
public string? ParentUserName { get; init; }
public int GroupId { get; init; }
public UserAdminLevel? SaLevel { get; init; }
public string? OemId { get; init; }
public string? Authority { get; init; }
public int ShopPointsPerMonth { get; init; }
public decimal? PostagePerOrder { get; init; }
public decimal? RefundPostagePerOrder { get; init; }
public decimal? GiftOemAmountPerOrder { get; init; }
public decimal? SyncLogisticsPerOrder { get; init; }
public decimal Points { get; init; }
public decimal? Balance { get; init; }
}
@@ -1,4 +1,7 @@
using Prism.Commands;
using Store.Management.Framework.Mappers;
using Store.Management.Framework.ViewModels;
using Store.Management.Models;
using Prism.Commands;
using Prism.Services.Dialogs;
using Store.Management.Api.Transport;
using Store.Management.Api.Users;
@@ -32,7 +35,7 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
new("裂变调试"), new("学员数据"), new("定向品牌"), new("活动库存")
];
private UserInfo? user;
private UserItemViewModel? user;
private string? passwordHash;
private string? confirmationHash;
private string? oldPasswordHash;
@@ -58,7 +61,7 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
public override void OnDialogOpened(IDialogParameters parameters)
{
user = parameters.GetValue<UserInfo?>("user");
user = parameters.GetValue<UserItemViewModel?>("user");
UserName = user?.UserName ?? "";
Draft = user == null ? new UserCreateRequest() : new UserUpdateRequest
{
@@ -146,7 +149,7 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
}
else
{
user = await userService.UpdateAsync((UserUpdateRequest)Draft);
user = UserMapper.ToItemViewModel(await userService.UpdateAsync((UserUpdateRequest)Draft));
profileSaved = true;
if (passwordHash != null)
{
@@ -1,8 +1,10 @@
using Prism.Commands;
using Store.Management.Framework.Mappers;
using Store.Management.Framework.ViewModels;
using Store.Management.Models;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using Prism.Services.Dialogs;
using Store.Management.Api.Models;
using Store.Management.Api.Transport;
using Store.Management.Api.Users;
using Store.Management.Framework.Controls;
@@ -11,6 +13,7 @@ using Store.Management.Views;
using Store.Management.Views.Dialogs;
using System.Windows;
using System.Windows.Controls.Primitives;
using Store.Management.Api.Models;
namespace Store.Management.ViewModels;
@@ -27,8 +30,8 @@ public sealed class UserManagementPageViewModel(IUserService userService, IDialo
public bool CanInteract { get => _canInteract; private set => SetProperty(ref _canInteract, value); }
public int Page { get; private set => SetProperty(ref field, value); } = 1;
public int Total { get; private set => SetProperty(ref field, value); }
public IReadOnlyList<UserInfo> Users { get; private set => SetProperty(ref field, value); } = [];
public UserInfo? SelectedUser { get; set => SetProperty(ref field, value); }
public IReadOnlyList<UserItemViewModel> Users { get; private set => SetProperty(ref field, value); } = [];
public UserItemViewModel? SelectedUser { get; set => SetProperty(ref field, value); }
public AsyncDelegateCommand SearchCommand => field ??= new(() => LoadAsync(Keyword.Trim(), SelectedGroup, 1, pageSize));
public AsyncDelegateCommand<DataGridPageRequest> PageCommand => field ??= new(
@@ -76,7 +79,7 @@ public sealed class UserManagementPageViewModel(IUserService userService, IDialo
appliedGroup = group;
pageSize = size;
SelectedUser = null;
Users = result.Items;
Users = UserMapper.ToItemViewModels(result.Items);
Total = (int)Math.Min(result.Total, int.MaxValue);
Page = request.Page;
@@ -100,7 +103,7 @@ public sealed class UserManagementPageViewModel(IUserService userService, IDialo
}
}
private async Task OpenEditorAsync(UserInfo? user)
private async Task OpenEditorAsync(UserItemViewModel? user)
{
ButtonResult result = ButtonResult.None;
dialogs.ShowDialog(nameof(UserEditorDialog), new DialogParameters { { "user", user } },
@@ -113,7 +116,7 @@ public sealed class UserManagementPageViewModel(IUserService userService, IDialo
private async Task DeleteAsync()
{
UserInfo user = SelectedUser!;
UserItemViewModel user = SelectedUser!;
if (MessageBox.Show($"确定删除用户“{user.UserName}”?", "删除用户", MessageBoxButton.OKCancel,
MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
{
@@ -1,4 +1,5 @@
using Prism.Commands;
using Store.Management.Framework.ViewModels;
using Prism.Commands;
using Prism.Services.Dialogs;
using Store.Management.Api.Transport;
using Store.Management.Api.Users;
@@ -36,7 +37,7 @@ public partial class UserBillingDialog : UserControl
}
}
public sealed class UserBillingDialogViewModel(IUserService userService) : ConfirmDialogViewModel<UserInfo>
public sealed class UserBillingDialogViewModel(IUserService userService) : ConfirmDialogViewModel<UserItemViewModel>
{
public sealed class BillingField(string label, decimal? original, decimal minimum,
Action<UserBillingUpdateRequest, decimal> apply, bool integer = false)
@@ -4,7 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:users="clr-namespace:Store.Management.Api.Users;assembly=Store.Management.Api"
xmlns:users="clr-namespace:Store.Management.Models;assembly=Store.Management.Models"
xmlns:vm="clr-namespace:Store.Management.ViewModels"
xmlns:wpf="MAX.WPF"
Width="500"
@@ -6,7 +6,7 @@
xmlns:f="Store.Management.Framework"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:users="clr-namespace:Store.Management.Api.Users;assembly=Store.Management.Api"
xmlns:users="clr-namespace:Store.Management.Models;assembly=Store.Management.Models"
xmlns:vm="clr-namespace:Store.Management.ViewModels"
xmlns:wpf="MAX.WPF"
Margin="5"
+1
View File
@@ -17,6 +17,7 @@
<File Path="AGENTS.md" />
<File Path="Directory.Build.props" />
</Folder>
<Project Path="Source/Store.Management.Models/Store.Management.Models.csproj" />
<Project Path="Source/Store.Management.Api/Store.Management.Api.csproj" />
<Project Path="Source/Store.Management.Framework/Store.Management.Framework.csproj" />
<Project Path="Source/Store.Management/Store.Management.csproj" />
@@ -1,3 +1,4 @@
using Store.Management.Models;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Text;
@@ -1,3 +1,5 @@
using Store.Management.Framework.ViewModels;
using Store.Management.Models;
using Autofac;
using Store.Management.Framework.Services;
using System.Windows.Controls;
@@ -145,14 +147,14 @@ class Program
await usersClient.Auth.LoginAsync(new() { UserName = "admin", Password = "320BB1D43EC042096A83DBAC6DC9294A" });
var usersPage = new UserManagementPage { DataContext = usersVm };
var grid = ((DockPanel)usersPage.Content).Children.OfType<DataGridEx>().Single();
var sample = new UserInfo { GroupId = 4, SaLevel = UserAdminLevel.SuperAdmin, OemId = "A01", Authority = "在线寄件,商品采集" };
var sample = new UserItemViewModel { GroupId = 4, SaLevel = UserAdminLevel.SuperAdmin, OemId = "A01", Authority = "在线寄件,商品采集" };
foreach (var (header, expected) in new[] { ("分组", "客服"), ("代理编码", "A01"), ("超管等级", "超级管理员"), ("权限", "在线寄件,商品采集") })
{
var column = grid.Columns.OfType<DataGridTextColumn>().Single(c => Equals(c.Header, header));
var cell = new TextBlock { DataContext = sample };
cell.SetBinding(TextBlock.TextProperty, column.Binding);
Check(cell.Text == expected, $"用户列表{header}显示错误:{cell.Text}");
cell.DataContext = new UserInfo { GroupId = 5 };
cell.DataContext = new UserItemViewModel { GroupId = 5 };
Check(cell.Text == (header == "分组" ? "5" : ""), $"用户列表{header}历史数据或空值显示错误");
}
var groupFilter = LogicalTreeHelper.FindLogicalNode(usersPage, "GroupFilter") as ComboBox;
@@ -165,10 +167,11 @@ class Program
"行菜单绑定编辑、删除和创建用户命令");
Check(groupFilter!.Text == "全部", "默认显示全部用户组");
groupFilter.SelectedIndex = 1;
Check(usersVm.SelectedGroup == Store.Management.Api.Users.UserGroup.MainAccount, "选择主号更新搜索条件");
Check(usersVm.SelectedGroup == UserGroup.MainAccount, "选择主号更新搜索条件");
Task search = usersVm.SearchCommand.Execute();
Check(!usersVm.CanInteract && status.Current.Level == StatusLevel.Info, "查询期间禁用操作");
await search;
Check(usersVm.Users.Single() is { Id: 1, UserName: "admin", Balance: null }, "查询结果映射为列表行");
Check(usersVm.CanInteract && usersVm.Total == 51 && status.Current.Level == StatusLevel.Success, "重试查询成功");
var narrowWindow = new Window { Content = usersPage, Width = 900, Height = 560, ShowInTaskbar = false };
narrowWindow.Show();
@@ -189,7 +192,7 @@ class Program
await usersVm.SearchCommand.Execute();
Check(handler.LastGroupId == null && usersVm.Page == 1, "全部清除分组并返回首页");
var billing = new Store.Management.Views.Dialogs.UserBillingDialogViewModel(userService);
billing.OnDialogOpened(new DialogParameters { { "parameter", new UserInfo
billing.OnDialogOpened(new DialogParameters { { "parameter", new UserItemViewModel
{
Id = 7, UserName = "billing", ShopPointsPerMonth = 45, PostagePerOrder = 3.5m,
RefundPostagePerOrder = 4m, GiftOemAmountPerOrder = null, SyncLogisticsPerOrder = 0m
@@ -248,7 +251,7 @@ class Program
"新增子账号提交手机号、分组、代理和权限");
editor.OnDialogClosed();
editor = new UserEditorDialogViewModel(userService);
editor.OnDialogOpened(new DialogParameters { { "user", new UserInfo { Id = 1, UserName = "old-login",
editor.OnDialogOpened(new DialogParameters { { "user", new UserItemViewModel { Id = 1, UserName = "old-login",
GroupId = 3, Authority = "微信回采,历史权限", Mobile = "13800000000", NickName = "测试" } } });
Check(editor.Permissions[0].IsSelected, "编辑回填权限");
editor.Permissions[0].IsSelected = false;