Add XCode skills for entity caching, ORM, and sharding ETL
|
---
name: cube-mvc-backend
description: >
使用 NewLife.Cube MVC 版本(NewLife.CubeNC)开发后台管理系统,涵盖 Area 区域注册、
EntityController<T> 实体控制器CRUD、字段定制机制(ListFields/FormFields/SearchFields/DetailFields)、
视图重载覆盖机制(ThemeViewLocationExpander + CubeEmbeddedFileProvider)、
菜单与权限体系(MenuAttribute/EntityAuthorizeAttribute/PermissionFlags),
以及 AddCube()/UseCube() 启动配置入口。
适用于基于 Cube MVC 框架开发后台管理系统、控制器扩展、视图覆盖、字段定制等任务。
argument-hint: >
说明业务场景:是新建 Area 区域、扩展控制器、还是定制字段/视图;
如涉及权限,说明需要哪类权限检查(查看/新增/编辑/删除)。
---
# Cube MVC 后台管理系统
## 适用场景
- 基于 NewLife.CubeNC(含 `#if MVC` 编译符的项目)开发管理后台。
- 新建 Area 区域并自动注册菜单与权限。
- 继承 `EntityController<T>` 开发实体 CRUD 页面并定制字段显示。
- 覆盖魔方内置视图(通过本地物理文件优先机制)。
- 配置菜单特性、权限过滤器、数据权限中间件。
---
## 快速启动
### 1. 服务注册(Program.cs / Startup.cs)
```csharp
var builder = WebApplication.CreateBuilder(args);
// 添加魔方核心服务(包含认证、模型绑定、菜单、权限、缓存等)
builder.Services.AddCube();
// 添加 Razor Pages 和 MVC
builder.Services.AddControllersWithViews();
var app = builder.Build();
// 激活魔方中间件管道(认证、静态文件、路由、数据权限等)
app.UseCube(builder.Environment);
// 映射控制器路由
app.MapControllerRoute(
name: "default",
pattern: "{area:exists}/{controller=Index}/{action=Index}/{id?}");
app.Run();
```
### 2. 项目模板方式
```powershell
# 安装模板
dotnet new install NewLife.Templates
# 创建带魔方的 Web 项目
dotnet new cube -n MyCompanyWeb
# 添加数据层引用
dotnet add reference ../MyCompany.Data/MyCompany.Data.csproj
```
---
## Area 区域注册
每个业务模块对应一个 Area,Area 注册时会自动扫描控制器、创建菜单树、绑定权限。
### AreaBase 继承规范
```csharp
// Areas/Order/OrderAreaRegistration.cs
[DisplayName("订单管理")] // 菜单显示名
[Menu(50, true, Icon = "fa-shopping-cart", // Order=50,可见=true
LastUpdate = "20240601")] // 有新菜单项时触发重建
public class OrderArea : AreaBase
{
public OrderArea() : base("Order") { } // 必须传入与文件夹同名的区域名
}
```
**命名约定**:类名必须以 `Area` 结尾,去掉 `Area` 后缀即为区域名(`OrderArea` → `Order`)。
**`AreaBase` 做了什么**:
1. 自动调用 `MenuHelper.ScanController()` 反射扫描本区域所有控制器。
2. 依据 `[MenuAttribute]` 和 `[DisplayName]` 创建或更新数据库菜单记录。
3. 将菜单ID写入 `CubeService.AreaNames`,供权限过滤器判断。
### Menu 特性参数
| 参数 | 说明 |
|------|------|
| `Order`(第1参数) | 排序值,**越大越靠前**(Admin=-1,Cube=-2) |
| `Visible`(第2参数) | 是否在导航中显示 |
| `Icon` | FontAwesome 图标名,如 `fa-table` |
| `LastUpdate` | 日期字符串,内置菜单有改动时需更新此值以触发重建 |
---
## EntityController<T> 实体控制器
### 继承与装饰
```csharp
// Areas/Order/Controllers/OrderController.cs
[Menu(100, true, Icon = "fa-list")] // 控制器在菜单中的位置与可见性
[OrderArea] // 声明所属区域(AreaBase 子类特性)
public class OrderController : EntityController<Order>
{
// 若实体与视图模型不同,使用泛型二参数形式
// public class OrderController : EntityController<Order, OrderModel>
}
```
### 静态构造器字段配置(核心模式)
所有字段定制均在 **静态构造器** 中完成,仅执行一次:
```csharp
static OrderController()
{
// ===== 列表字段 =====
ListFields.RemoveCreateField() // 删除 CreateUser/CreateTime
.RemoveUpdateField() // 删除 UpdateUser/UpdateTime
.RemoveRemarkField(); // 删除 Remark
ListFields.RemoveField("Secret"); // 删除指定字段(支持逗号分隔多个)
// 在 Remark 前插入自定义链接列
var df = ListFields.AddListField("detail", null, "Remark");
df.DisplayName = "查看详情";
df.Url = "OrderDetail?orderId={Id}"; // {Id} 自动替换为当前行实体的属性值
df.Target = "_blank"; // 在新标签打开
df.DataAction = "action"; // 使用 ajax 方式(不跳转,执行动作)
// 自定义单元格值(委托)
var sf = ListFields.GetField("Status") as ListField;
sf.GetValue = (entity) => ((Order)entity).StatusName; // 返回值覆盖默认显示
// ===== 添加表单字段 =====
AddFormFields.RemoveField("Id,CreateTime,CreateUser,UpdateTime,UpdateUser");
AddFormFields.GetField("Title").Required = true; // 设置必填
// ===== 编辑表单字段 =====
EditFormFields.RemoveField("CreateTime,CreateUser");
EditFormFields.AddField("AuditTime"); // 从 AllFields 中补充字段
// ===== 搜索字段 =====
SearchFields.AddField("Status").Multiple = true; // 多选下拉
SearchFields.AddField("CreateTime");
// ===== 详情字段 =====
DetailFields.AddField("Remark");
}
```
### 常用字段操作方法速查
| 方法 | 说明 |
|------|------|
| `RemoveField(params string[])` | 删除字段,支持 `*` 模糊匹配 |
| `RemoveCreateField()` | 批量删除 CreateUser/CreateTime |
| `RemoveUpdateField()` | 批量删除 UpdateUser/UpdateTime |
| `RemoveRemarkField()` | 批量删除 Remark |
| `AddField(name)` | 从实体 AllFields 添加指定字段 |
| `AddListField(name, before, after)` | 插入自定义列表列(ListField 类型) |
| `AddFormField(name, before, after)` | 插入自定义表单项(FormField 类型) |
| `GetField(name)` | 获取字段对象(可强转为 ListField/FormField 等) |
| `Replace(oriName, newName)` | 替换字段 |
### ListField 扩展属性
```csharp
var lf = ListFields.GetField("Name") as ListField;
lf.Header = "订单编号"; // 列头文字(覆盖 DisplayName)
lf.Url = "/Order/Detail/{Id}"; // 单元格链接,{属性名} 自动插值
lf.Target = "_blank"; // 链接目标
lf.TextAlign = TextAligns.Center; // 对齐方式
lf.Class = "text-warning"; // 单元格 CSS 类
lf.MaxWidth = 200; // 超出宽度折叠
lf.DataAction = "action"; // "action" = ajax调用,null = 普通跳转
lf.GetValue = e => ((Order)e).FormatAmount(); // 自定义显示值
lf.GetClass = e => ((Order)e).IsOverdue ? "text-red" : ""; // 动态样式
```
### 搜索与数据加载钩子
```csharp
// 重载搜索逻辑(调用实体 Search 方法)
protected override IEnumerable<Order> Search(Pager p)
{
return Order.Search(p["key"], p["status"].ToInt(-1), p);
}
// 重载单条查询
protected override Order FindData(String id)
{
return Order.FindByKey(id.ToInt());
}
```
### CRUD 钩子方法
```csharp
// 保存前验证(新增与编辑均触发)
protected override Boolean Valid(Order entity, DataObjectMethodType type, Boolean post)
{
if (post && entity.Amount <= 0)
throw new Exception("金额必须大于0");
return base.Valid(entity, type, post);
}
// 新增前处理
protected override void OnInsert(Order entity)
{
entity.CreateUserId = ManageProvider.User?.ID ?? 0;
base.OnInsert(entity);
}
// 更新前处理
protected override void OnUpdate(Order entity)
{
entity.UpdateTime = DateTime.Now;
base.OnUpdate(entity);
}
// 删除前处理
protected override void OnDelete(Order entity)
{
if (entity.Status == 2) throw new Exception("已完成订单不允许删除");
base.OnDelete(entity);
}
```
---
## 视图覆盖机制
### 覆盖优先级(从高到低)
应用程序本地物理文件 **始终优于** 魔方嵌入式资源:
```
1. ~/Areas/{Area}/Views/{Controller}_{Theme}/{Action}.cshtml ← 主题特定覆盖
2. ~/Areas/{Area}/Views/{Controller}/{Action}.cshtml ← 控制器覆盖
3. ~/Areas/{Area}/Views/{Theme}/{Action}.cshtml ← 主题共享覆盖
4. ~/Areas/{Area}/Views/Shared/{Action}.cshtml ← 区域共享覆盖
5. ~/Views/{Theme}/{Action}.cshtml ← 应用级主题覆盖
6. ~/Views/Shared/{Action}.cshtml ← 应用级共享覆盖
7. 魔方程序集嵌入资源(fallback)
```
**`{Theme}`** 由 `CubeSetting.Current.Theme` 决定(默认 `ACE`),首页使用 `CubeSetting.Current.Skin`。
### 常见视图覆盖场景
```
# 覆盖列表页(Order 区域 Product 控制器)
Areas/Order/Views/Product/Index.cshtml
# 覆盖编辑表单(共享给该区域所有控制器)
Areas/Order/Views/Shared/EditForm.cshtml
# 覆盖特定主题下的列表页
Areas/Order/Views/Product_ACE/Index.cshtml
# 覆盖全局共享视图(影响所有区域)
Views/ACE/_LayoutAdmin.cshtml
```
### 静态资源覆盖
通过物理文件覆盖嵌入资源中的 JS/CSS/图片:
```
# 项目 wwwroot 中同路径文件会优先于魔方嵌入资源
wwwroot/js/jquery.min.js → 覆盖嵌入的同名文件
wwwroot/css/custom.css → 新增自定义样式
```
---
## 菜单与权限
### PermissionFlags 枚举
| 值 | 含义 | 适用 Action |
|----|------|------------|
| `Detail` (0x01) | 查看 | Index、Detail |
| `Insert` (0x02) | 新增 | Add (GET/POST) |
| `Update` (0x04) | 编辑 | Edit (GET/POST) |
| `Delete` (0x08) | 删除 | Delete |
| `Approve` (0x10) | 审批 | 自定义 Action |
| `Export` (0x20) | 导出 | Export |
### EntityAuthorize 用法
```csharp
// 方法级权限(自定义 Action)
[EntityAuthorize(PermissionFlags.Approve)]
public ActionResult Approve(Int32 id)
{
// ...
}
// 允许匿名访问(跳过权限检查)
[AllowAnonymous]
public ActionResult Public()
{
// ...
}
```
`EntityController<T>` 内置 5 个标准 Action 已自动配置对应权限,无需手动标注。
### 权限检查流程
```
请求到达 → EntityAuthorizeAttribute.OnAuthorization()
↓
检查 [AllowAnonymous] → 有则跳过
↓
根据控制器命名空间映射到菜单节点
↓
user.Has(menu, PermissionFlags) → XCode Membership 实现
↓
无权限 → JSON 请求返回 401/403;浏览器请求跳转登录页
```
---
## 数据权限(多租户隔离)
`DataScopeMiddleware` 在每次请求时自动设置当前用户的数据权限上下文,XCode 查询会自动附加过滤条件。
```csharp
// 中间件自动注入,无需手动调用
// UseCube() 内部已注册 UseMiddleware<DataScopeMiddleware>()
// 控制器内可读取当前租户
var tenantId = TenantContext.CurrentId;
// 若需要跳过数据权限过滤(超级管理员场景)
using (DataScopeContext.Disable())
{
var all = Order.FindAll(); // 此处查询不带数据权限过滤
}
```
---
## CubeSetting 关键配置
`CubeSetting` 通过 `[Config("Cube")]` 绑定,会自动从 `appsettings.json` 或 Cube 配置文件读取。
```csharp
var set = CubeSetting.Current;
// 读取常用配置
var theme = set.Theme; // 主题名(默认 ACE)
var skin = set.Skin; // 首页皮肤
var uploadDir = set.UploadPath; // 上传目录(默认 Uploads)
var avatarDir = set.AvatarPath; // 头像目录(默认 Avatars)
var jwtSecret = set.JwtSecret; // JWT 签名密钥
var expire = set.TokenExpire; // Token 有效期(秒,默认 7200)
var maxErr = set.MaxLoginError; // 登录失败上限(默认 5)
```
**配置文件示例(appsettings.json):**
```json
{
"Cube": {
"Theme": "Tabler",
"TokenExpire": 86400,
"CorsOrigins": "https://app.example.com",
"MaxLoginError": 3,
"JwtSecret": "your-secret-key-here"
}
}
```
---
## 常见例外与注意事项
- `ListFields.AddListField()` 返回的是 `ListField` 类型,但存在 `GetField()` 返回 `DataField`,**需要强转**才能访问 `Url`/`GetValue` 等扩展属性。
- 静态构造器中对字段集合的修改是**全局一次性**操作,不能在实例方法中修改(否则线程不安全)。
- 视图文件名含 `-` 或 `@` 时,嵌入资源中对应名称会被映射为 `_`(`CubeEmbeddedFileProvider` 自动反向映射)。
- `[Menu]` 的 `LastUpdate` 字段若不更新,新添加的控制器菜单项不会触发菜单重建;建议每次改动菜单结构时更新此值。
- 区域名大小写需与文件夹名**严格一致**(`base("Order")` vs 文件夹 `Areas/Order/`)。
---
## 推荐检查项
- [ ] `AreaBase` 子类静态构造器中是否已调用 `RegisterArea(typeof(XxxArea))`(或通过 `base()` 自动完成)
- [ ] 控制器是否标注了所属区域的特性(如 `[OrderArea]`)
- [ ] 静态构造器字段配置是否放在 `static XxxController(){}` 中(非实例构造器)
- [ ] 覆盖视图路径是否与 `ThemeViewLocationExpander` 查找顺序严格一致
- [ ] 自定义 Action 是否标注了 `[EntityAuthorize]` 或 `[AllowAnonymous]`
- [ ] `CubeSetting.JwtSecret` 在生产环境中是否已配置为强密钥
## 待确认问题
- `EntityTreeController`(树形控制器)与 `EntityController` 的字段配置方式是否完全相同。
- `ReadOnlyEntityController` 是否支持 `AddListField` 等写操作相关的字段定制。
|