Identity.Infrastructure 开发指南
Infrastructure 层为 Identity 及所有业务模块提供一组可复用的横切能力,而非某个特定业务功能。它解决的是"文件怎么存、配置怎么读、附件怎么挂、字典怎么管"这类通用问题,任何模块(CmsKit、Platform 等)都可以直接消费这些服务。
本指南聚焦功能与用法,不罗列实体字段。涉及到的实体仅用于帮助理解数据模型。
| 能力 | 一句话说明 | 主要服务接口 |
|---|---|---|
| 文件存储 Files | 统一文件上传/下载,支持本地、七牛、Sosoos、CloudFlare R2,按内容哈希秒传去重 | IFileService / IFileManager / IAvatarService |
| 设置系统 Settings | 分层(用户/租户/系统/角色/配置)键值配置,运行时可覆盖 | ISettingManager / ISettingService |
| 设置定义 SettingDefinitions | 配置的元 Schema,驱动前端动态表单 | ISettingDefinitionManager / ISettingDefinitionService |
| 通用附件 CommonAttachments | 把文件挂载到任意业务实体(文章封面、内容图等),带类型与权限校验 | ICommonAttachmentService / ICommonAttachmentManager |
| 数据字典 BaseType / BaseItem | 通用枚举字典(性别、状态码等),前端下拉数据源 | IBaseTypeService / IBaseItemService |
1. 文件存储(Files)
能力概述
文件上传后生成 KitFile 记录(存储路径、大小、后缀、哈希等),并通过 KitFileMeta 按 MD5 内容哈希聚合同一份文件在不同存储后端(本地 / R2 / 七牛)的多条记录,实现秒传去重:相同内容第二次上传不会重复落盘,只新增一条引用。
支持的存储后端由 FileUploadType 枚举描述:Local / Qiniu / Sosoos / Aliyun / Tencent / CloudFlare / Aws,通过 IFileStorageOptionsResolver 解析 ServiceName 选择具体实现(LocalFileManager、QiniuManager、SosoosManager、CloudFlareR2Manager,均以 keyed 方式注册)。
核心服务
IFileService:上传与文件 CRUD。UploadAsync有多种重载——IFormFile、字节数组、Stream、远程 URL;也支持批量上传(受NumLimit/MaxFileSize约束)。IFileManager:根据Path或FileMetaId解析可访问的完整Url,支持批量回填、缓存与头像 URL 解析(FillAvatarUrlsAsync/BindFileUrls)。业务表只存Path或FileMetaId,展示时统一通过它取 URL。IAvatarService:基于 SkiaSharp 生成文字头像(GenerateAvatar)、按名字复用同一头像(CreateAvatarUrlByNameAsync,MD5 命名避免重复)、随机头像(GetRandomAvatar)。
HTTP API(FileController,/api/identity/file)
| 方法 | 路由 | 说明 |
|---|---|---|
| POST | /uploads([FromForm] List<IFormFile>) | 多文件上传 |
| POST | /upload(IFormFile file) | 单文件上传 |
| POST | /upload-from-url(string url) | 按远程 URL 抓取上传 |
| GET | /{id:long} | 文件详情(含 Url) |
| GET | / | 分页列表(Name/Type/IsFileExist 过滤) |
| PUT | /{id:long} | 仅改文件名 |
| DELETE | /{id:long} | 删除 |
| PUT | /check_all_file_exists | 校验本地文件实际是否存在 |
| GET | /check_file_exists/{id:long} | 校验单个文件 |
| GET | /create_avatar?name=(AllowAnonymous) | 返回 PNG 字节流头像 |
| GET | /create_avatar_url_by_name?name= | 生成并上传头像,返回 FileDto |
| GET | /get_random_avatar(AllowAnonymous) | 随机头像 |
配置(FileStorage 节)
{
"FileStorage": {
"MaxFileSize": 10485760,
"NumLimit": 10,
"Include": "jpg,jpeg,png,gif,mp4,mp3,pdf",
"Exclude": "exe,bat",
"ServiceName": "CloudFlareR2",
"LocalFile": { "RootPath": "wwwroot/files", "PrefixPath": "files", "Host": "" },
"CloudflareR2": {
"AccountId": "", "AccessKey": "", "SecretKey": "",
"Bucket": "", "PrefixPath": "files", "Host": "https://cdn.example.com", "UseHttps": true
}
}
}
运行时覆盖:
FileStorageOptionsResolver会合并Setting(键FileStorage.Config,JSON)中的值到上述配置,变更 R2 凭据时自动刷新 DI 中的单例客户端。即无需改 appsettings 即可热更新存储配置。
开发示例
// 1) 上传(例如 TTS 音频,按业务前缀隔离)
var file = await _fileService.UploadAsync(audioBytes, "speech.wav", key: 0, prefixPath: "tts");
// file.Path / file.Url / file.Id 可存入业务表
// 2) 展示时解析完整 URL
var url = await _fileManager.GetFileUrlAsync(file.Path);
# 单文件上传
curl -X POST https://localhost:7002/kit_api/api/identity/file/upload \
-F "file=@/path/to/cover.png"
2. 设置系统(Settings)
分层 Provider 模型
设置值(Setting)按来源 Provider分层,取值优先级从高到低为:
用户 (U) > 租户 (T) > 系统 (S) > 配置文件 (C)
全部未命中时回落到 SettingDefinition.DefaultValue。各层由对应的 SettingValueProvider 实现:
UserSettingValueProvider(U,按ProviderKey=用户Id)TenantSettingValueProvider(T)SystemSettingValueProvider(S)ConfigurationSettingValueProvider(C,读IConfiguration,键名.自动转:)
ISettingManager 按优先级聚合取值,启动时还会把 appsettings 中已定义但库中缺失的 S 级配置种子化(标记 BuiltInParams,禁止删除/改键)。
核心服务
ISettingManager:读配置的主力。GetOrNullAsync(name)/GetOrNullAsync<T>()、GetSystemBoolAsync/GetSystemIntAsync/GetSystemStringListAsync、IsTrueAsync、FindAsync。ISettingService:写配置与 CRUD。SetKeyValuesAsync不存在则建、存在则更新(JSON 类型会做语法与字段校验),并保护内置参数不被覆盖。
HTTP API
用户侧(SettingController,/api/identity/setting)
| 方法 | 路由 | 说明 |
|---|---|---|
| POST | /u/key_values(IDictionary<string,string>) | 批量设置当前用户配置 |
| GET | /u/key/{key} | 取当前用户某键 |
| GET | /u/keys?keys[]= | 批量取当前用户 |
| GET | /u/key/{key}/{userId} | 取指定用户某键(仅可见定义) |
| GET | /u/keys/{userId}?keys[]= | 批量取指定用户 |
| GET | /visible/grouped | 按 Group/SubGroup 分组返回可见定义(前端动态表单用) |
管理侧(SettingAdminController,/api/identity/setting/admin,需 Identity.Settings.*)
| 方法 | 路由 | 权限 |
|---|---|---|
| GET | / | Settings.GetList |
| GET | /{id:guid} | Settings.GetList |
| POST | / | Settings.Create |
| PUT | /{id:guid} | Settings.Update |
| DELETE | /{id:guid} | Settings.Delete |
| POST | /{id:guid}/reset | Settings.Delete(回到默认值) |
| GET | /json-fields?name=&providerName=&providerKey= | 把 JSON 设置展平为字段列表 |
| PUT | /json-field(UpdateJsonFieldReq) | 按路径(如 CloudflareR2.Host)更新 JSON 内单字段 |
开发示例
// 读取系统级配置(类型安全)
var host = await _settingManager.GetOrNullAsync("MailKitOptions.Host", SettingProviderNameEnum.S);
var captchaOn = await _settingManager.GetSystemBoolAsync("Captcha.Enabled");
// 写入系统级配置
await _settingService.SetKeyValuesAsync(new SettingCreateUpdateReq
{
Name = "MailKitOptions.Host",
Value = "smtp.example.com",
ProviderName = SettingProviderNameEnum.S
});
常用定义名见
Runtime/IdentitySettingNames.cs,如Captcha.Enabled、MailKitOptions.Host、FileStorage.Config、MeiliSearch.Host、RateLimit.Email.*等。读取时把键中的.替换为:查IConfiguration。
3. 设置定义(SettingDefinitions)
定义 vs 值
SettingDefinition= 配置的元 Schema:名称、默认值、数据类型、分组、是否加密、是否对客户端可见、JSON Schema。Setting= 某个 Provider 下的实际值。
这与 ABP 的 Setting 体系一致:定义描述"有哪些可配置项、长什么样",值记录"当前配了什么"。
注册定义
定义通过代码中的 ISettingDefinitionProvider 声明,首次启动同步入库(增量同步,不覆盖你在后台维护的展示文案);若 DataType=json 且含 JsonSchemaType,会自动从 CLR 类型递归生成 JSON Schema。
public class MySettingDefinitionProvider : ISettingDefinitionProvider
{
public void Define(ISettingDefinitionContext context)
{
context.Add(new SettingDefinition("My.Feature.Enabled", "false", "功能开关")
.WithDataType("bool")
.WithGroupName("高级")
.WithVisibility(true));
}
}
// 宿主启动时通过反射自动注册所有 ISettingDefinitionProvider
内置实现 IdentitySettingDefinitionProvider 已定义 OAuth、验证码、邮件、MeiliSearch、文件存储、限流等全部系统设置。
前端动态表单
拉取可见定义即可按 DataType / Options / GroupName 自动渲染输入控件:
curl https://localhost:7002/kit_api/api/identity/settingdefinition/visible
HTTP API(SettingDefinitionController,/api/identity/settingdefinition)
| 方法 | 路由 | 说明 |
|---|---|---|
| GET | /visible(AllowAnonymous) | 可见定义列表 |
| GET | /by-name/{name} | 按名取定义 |
| GET | / | 分页列表(可按 Name/Group/SubGroup/IsVisible 过滤) |
| GET | /{id:guid} | 详情 |
| POST | / | 新建 |
| PUT | /{id:guid} | 更新 |
| DELETE | /{id:guid} | 删除 |
4. 通用附件(CommonAttachments)
能力概述
通用附件把"文件"挂载到任意业务实体上(如文章封面、文章内容图、视频等)。它不存储文件本身,只存 FilePath / FileMetaId,文件实体仍在 KitFile 中——因此与 Files 能力完全打通:查询附件时自动回填 FileUrl。附件类型由扩展名推断(Image/Video/Audio/Document/Other)。
配置实体类型与访问校验
消费方需在模块启动时声明"哪些实体类型允许挂附件",以及分类规则与权限校验器:
services.Configure<CommonAttachmentOptions>(o =>
{
o.EntityTypes.Add(new CommonFileEntityTypeDefinition(nameof(Article))
.WithAccessValidator<ArticleAccessValidator>() // 上传前校验实体归属权限
.WithCategory("Article/Cover", AttachmentType.Image) // 封面:仅图片
.WithCategory("Article/Content", // 内容:多种类型
AttachmentType.Image, AttachmentType.Video,
AttachmentType.Audio, AttachmentType.Document, AttachmentType.Other));
});
上传/查询会经过 ICommonAttachmentValidator:校验实体类型是否允许挂附件、当前用户是否有权操作该实体,非法时抛 BusinessException / EntityCantHaveException。
HTTP API(CommonAttachmentController,/api/identity/common-attachment)
| 方法 | 路由 | 说明 |
|---|---|---|
| POST | /(AttachmentCreateInput) | 新增附件(需先上传文件拿到 Path) |
| DELETE | /{id:guid} | 删除(非 Admin 仅能删自己创建的) |
| GET | /(AllowAnonymous) | 分页列表(按 EntityId/EntityType/Category/AttachmentType 过滤,回填 FileUrl) |
| GET | /{entityId:guid} | 取某实体全部附件 |
| PUT | /check_all_file_exists | 批量校验文件存在性 |
典型使用流程
- 业务实体(如
Article)要支持附件时,在模块启动里Configure<CommonAttachmentOptions>注册CommonFileEntityTypeDefinition与访问校验器。 - 先走 Files 上传拿到
Path,再调用:
curl -X POST https://localhost:7002/kit_api/api/identity/common-attachment \
-H "Content-Type: application/json" \
-d '{
"entityId": "article-guid",
"entityType": "Article",
"category": "Article/Cover",
"attachmentType": 10,
"fileName": "cover.png",
"filePath": "files/202601/abc.png",
"fileSize": 12345
}'
- 查询时
GET /api/identity/common-attachment/{entityId},响应已带回FileUrl。
5. 数据字典(BaseType / BaseItem)
概念
通用数据字典,用于替代散落的硬编码枚举。关系为父子:
BaseType:字典分类(如"性别"),含唯一TypeCode。BaseItem:字典项(如"男/女"),含ItemCode/ItemName,可自引用形成层级,并支持ExtraProperties(JSON 扩展字典)。
开发者通常把 BaseType 当作"枚举组"、BaseItem 当作"枚举值",并通过 TypeCode 查询(自动转换为 BaseTypeId)。
HTTP API(plat 域)
| 方法 | 路由 | 说明 |
|---|---|---|
| GET | /api/plat/type/list(BaseTypeQuery 在 body) | 分类分页列表(IncludeBaseItems=true 一并加载子项) |
| GET | /api/plat/type/{id} | 分类详情 |
| POST | /api/plat/type(需 Platform.BaseTypes) | 新建分类 |
| PUT | /api/plat/type/{id}(需 Platform.BaseTypes) | 更新 |
| DELETE | /api/plat/type/{id}(需 Platform.BaseTypes) | 删除 |
| GET | /api/plat/item(AllowAnonymous) | 字典项分页列表 |
| GET | /api/plat/item/select(AllowAnonymous) | 返回 SelectMixedOption(Label=ItemName, Value=ItemCode),前端下拉用 |
| POST | /api/plat/item(需 Platform.BaseItems) | 新建字典项 |
| PUT | /api/plat/item/{id}(需 Platform.BaseItems) | 更新 |
| DELETE | /api/plat/item/{id}(需 Platform.BaseItems) | 删除 |
前端下拉用法
# 拿"性别"下拉选项,Value 即为 ItemCode
curl "https://localhost:7002/kit_api/api/plat/item/select?typeCode=Gender"
# => [{ "label": "男", "value": "Male", "extraProperties": null }, ...]
模块注册要点
IdentityInfrastructureModuleStartup 在启动时完成:
- 绑定
FileStorageOption与CloudflareR2Options配置; - 以 keyed 方式注册 4 个文件上传实现(
LocalFileManager/QiniuManager/SosoosManager/CloudFlareR2Manager); - 反射注册所有
ISettingDefinitionProvider,并注册ISettingDefinitionManager单例; - 注册各层
SettingValueProvider(Configuration 单例,User/Tenant/System 为 Scoped); - 启动时
SettingDefinitionManager.Initialize(...)同步定义,ISettingManager.InitializeFromConfigurationAsync()把 appsettings 种子化为系统设置。
消费方只需 AddFreeKitIdentity() 或对应模块启动类即可获得上述全部能力(具体接入方式见 Identity 模块索引)。