新的业务模块采用"受信任部署模块包"机制。它保持 ASP.NET Web Forms 与 .ascx 用户控件路线:模块源文件由受信任的开发、构建和部署流程写入站点,管理员只从已验证包中注册和启用模块。
当前版本不支持后台 ZIP/DLL 上传、在线编译、在线编辑模块文件、外部 URL、远程下载、自动加载 JavaScript 或包自带数据库迁移。已有平铺的 DesktopModules/*.ascx 和 Admin/*.ascx 继续作为 Legacy 模块运行,不要求批量迁移。
一个新模块包位于 src/Portal/DesktopModules/{PackageFolder}/,例如:
DesktopModules/
ModuleProbe/
ModuleProbe.ascx
ModuleProbe.ascx.cs
ModuleProbe.ascx.designer.cs
module.json
Styles/
ModuleProbe.css
PackageFolder 必须匹配 ^[A-Za-z][A-Za-z0-9_-]{0,63}$。moduleFolder 下的 module.json 当前对只读参考包(如 ModuleProbe)使用 schemaVersion: 1:
{
"schemaVersion": 1,
"packageId": "HIA.ModuleProbe",
"displayName": "模块验证 / Module Probe",
"version": "1.0.0",
"minimumPortalVersion": "1.0",
"desktopEntry": "DesktopModules/ModuleProbe/ModuleProbe.ascx",
"resources": [
"Styles/ModuleProbe.css"
]
}
schemaVersion现状(C-anp-P20 实测):ModuleProbe这类只读参考包用1;但所有业务模块(HIA.*)的 manifest 实际为2——PortalModuleControl<T>加载器要求capability块,而该块由schemaVersion: 2引入。Test-PortalBusinessModuleSmoke.ps1目前仍打印"期望schemaVersion: 1",属门禁口径陈旧(全部业务模块均为 2),不要为过门禁把业务模块降回 1。
规则如下:
packageId必须匹配^[A-Za-z][A-Za-z0-9_.-]{0,99}$,并在已部署包中保持唯一。desktopEntry必须是当前包目录内的现有.ascx文件,且必须通过门户既有的DesktopModules/路径校验。resources必须是包目录内的现有相对文件;允许.css、.png、.jpg、.jpeg、.gif、.webp。只有已声明 CSS 会由门户宿主自动去重挂载。- 清单不得出现
script、scripts、externalUrl、externalUrls、assembly、assemblies、packageUrl。这些能力需要未来的可信部署机制另行设计并审核。 - 新模块应继承
PortalModuleControl<T>,实现IPortalModuleControl。涉及公开 API、配置、安全边界或复杂流程时,使用标准 XML 注释中的中英双语段落。
入口控件的指令行使用全限定类名(命名空间为 ASPNET.StarterKit.Portal):
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="EmployeeProfileConfirm.ascx.cs" Inherits="ASPNET.StarterKit.Portal.EmployeeProfileConfirm" %>必须写全限定名。
Inherits只写类名时,标记层在运行期找不到类,会抛CS0103;而 msbuild 编译不出这个错误(代码后置已单独编译进bin/Portal.dll)。这类问题只能由运行期门禁发现,详见 开发指南 与Test-PortalAscxCompilationContract.ps1。
标记层与代码后置中的注释使用双语块:
<%--
<lang>
<zh-CN>说明:员工只确认自己当前绑定的低敏资料,不提供上传或外部资源。</zh-CN>
<en>Note: employees only confirm their currently bound low-sensitivity profile data; no upload or external resource capability is provided.</en>
</lang>
--%>权限键集中定义在 src/Portal.Components/PortalPermissions.cs,采用点分命名(领域.对象.动作):
public const string SettingsView = "Settings.View";
public const string SettingsEdit = "Settings.Edit";
public const string OpsDiagnosticsDetail = "Ops.Diagnostics.Detail";模块在代码后置中引用这些常量,不要在模块内硬编码字符串字面量:
ReviewRoleKey = PortalPermissionKeys.BusinessApplicationReview数据范围(可见行)由 Foundation 层统一控制:需要按组织子树过滤的模块,应使用员工目录查询的子树参数,而不是自行拼接过滤条件,避免绕过范围控制。
新增权限键时,还需同步数据库授权数据(src/Setup/PortalCfg_RolePermissions.sql 一类脚本),并在提交前运行 Test-PortalBusinessModuleSmoke.ps1 与相关授权门禁。
门户对语义标记有一致约定,新模块必须遵守(这些约定已有运行期门禁守护):
| 场景 | 正确写法 | 说明 |
|---|---|---|
| 数据表头 | <th scope="col"> |
不要留裸 <th> |
| 表单字段标签 | <label class="…" for="<%= Xxx.ClientID %>"> |
class 保持既有样式类,只加 for |
| 只读字段 | <dl> + <dt> + <dd> |
标签后跟展示值而非表单控件时,不要用 label for |
| 模块标题 | 保持元素名与 class,只加 role="heading" 与 aria-level |
前台桌面 aria-level="1";后台管理页 = "2" |
| 空态 | 走共享空态标记 | 不要自造"暂无数据"文案 |
两条容易做错的:
for只能指向表单控件。给只读展示字段加for是错误语义 —— 辅助技术会把标签关联到无关控件上,比不加更糟。只读字段用dl/dt/dd。- 不要为了语义改元素名。
asp:Label渲染为span,改成原生h1会改变视觉(除非另有 CSS 精确等价),且需新建自定义渲染控件。ARIA 标题是等价语义且视觉零变化。
CSS 应使用门户输出的稳定 scope(portal-module、portal-module-{id}、portal-pane-{pane}、portal-package-{packageId}),避免使用全局元素选择器污染其他模块;不要声明或随包分发专有字体,遵守项目的字体许可规则。
-
先在 Visual Studio 或 VSCode 构建解决方案,完成受信任部署流程后把包目录部署到
DesktopModules/。 -
对目标数据库执行
src/Setup/PortalCfg_ModulePackageStates.sql,或使用 SQL 兼容性脚本:& 'C:\Program Files\PowerShell\7\pwsh.exe' -NoLogo -NoProfile -File dev\scripts\Test-PortalSqlCompatibility.ps1 ` -ConnectionStringsConfigPath $testConfig -ApplyP3Migrations -RequireP3Migrations
-
以
Admins身份访问Admin/ModuleCatalog.aspx。目录页只显示已部署且通过校验的包;点击 Register 会创建指向 manifestdesktopEntry的旧模块定义记录。 -
在既有 Tab 布局管理页面从已注册定义中添加实例。新模块默认使用
CacheTimeout=0;缓存策略需要单独评估后再调整。 -
已注册包可在目录页 Enable 或 Disable。状态写入
PortalCfg_ModulePackageStates,并记录运营审计。
包状态表中不存在记录时,已验证包按启用处理,兼容尚未配置状态行的部署。状态表缺失或不可读时,前台同样保持默认启用,但后台状态写入会提示先执行迁移。
每个新模块都应接入门禁编排,并单独通过冒烟检查:
& 'C:\Program Files\PowerShell\7\pwsh.exe' -NoLogo -NoProfile -File dev\scripts\Test-PortalBusinessModuleSmoke.ps1 `
-ModuleName HIA.YourModule `
-SqlMigrationFile src\Setup\PortalBiz_YourEntities.sql冒烟门禁强制以下约束(均为 Fail 级,不可绕过):
| 约束 | 门禁检查名 |
|---|---|
包目录与 module.json 存在且可解析 |
Module directory / Module manifest |
desktopEntry 必须是包目录内的 .ascx,不得逃逸 |
Desktop entry safety / Desktop entry file |
| 资源文件必须存在且不得逃逸包目录 | Manifest resource safety / Manifest resource file |
| 包内不得有 DLL、ZIP、可执行文件或脚本文件 | Module package static asset policy |
默认禁止 JavaScript 文件(除非显式 -AllowModuleScripts) |
Module script policy |
默认禁止内联 <script> 块 |
Inline script policy |
业务迁移脚本必须命名为 PortalBiz_*.sql |
SQL migration file / SQL migration naming |
迁移脚本不得含 USE [database](必须可移植到任意库名) |
SQL migration portability |
packageId 建议 HIA.{ModuleName} 风格 |
Package id convention(Warning 级) |
version 需为 SemVer 形态 |
Module version(Warning 级) |
迁移文件按业务实体命名,不按模块命名。例如
HIA.MyWorkItems对应PortalBiz_WorkItems.sql。找不到对应文件时需用-SkipSqlMigrationCheck显式豁免,并在提交信息里写明理由 —— 这是如实豁免,不是为过门禁而编造。
模块包生命周期为:Available -> Registered -> Enabled 或 Disabled -> UninstallReady。
禁用只阻止该包的实例渲染和 CSS 挂载,不删除模块实例、模块业务数据或物理目录。目录页的 Preflight 会显示对应定义和实例数量。存在实例时,旧定义页会拒绝直接删除,必须先禁用、迁移或明确清理实例及其业务数据。
物理目录删除仍是受信任部署操作:先完成预检和实例清理,再在部署流程中移除包目录,最后重启应用或等待应用域刷新。后台不会删除模块物理目录。
ModuleProbe 是只读参考包,用于验证注册、启停、CSS、缓存身份和虚拟路径,不写入业务数据。排查新包时:
- 先确认
module.json、入口控件和声明资源都已部署,且路径大小写与清单一致。 - 在
Admin/ModuleCatalog.aspx确认包被发现,并执行 Preflight。 - 确认已注册定义和 Tab 实例存在;禁用状态下前台应跳过该包实例。
- 查看
Admin/DiagnosticsLogs.aspx中的ModulePackage.*事件。日志中不应依赖连接串、Cookie、Token 或密码。
常见故障与定位:
| 现象 | 优先排查 |
|---|---|
| 前台 500 且提示找不到类 | Inherits 是否写了全限定名(见「代码骨架」) |
| 包在目录页不显示 | module.json 是否已部署、路径大小写、是否被状态表禁用 |
| 页面有模块但样式未生效 | resources 是否声明了 CSS、selector 是否用了稳定 scope |
门禁报 Desktop entry safety |
desktopEntry 是否写了包目录外的路径或非 .ascx |
| 实例渲染但数据为空 | 是否绕过数据范围控制,或迁移脚本未执行 |
| 前台模块整页空白但构建通过 | 是否登记进 Portal.csproj 的 <Compile>;.ascx 是否用 CodeBehind;标记层类名与模块 CSS 是否一致;业务包是否加入 Portal.ModulePackages.Enabled |
| 模块在部分皮肤下样式丢失 | 对应皮肤的 Default.css 是否补了 .<模块前缀类> 主题作用域规则(仅模块自带 Styles/*.css 不够) |
以下四类缺陷构建与单测全绿时不会出现,只在运行期以"页面空白 / 门禁误判"形式暴露,是 C-anp-P20 交付两个业务模块时逐项踩出的。新增任何模块都先过一遍:
- 代码后置必须登记进
Portal.csproj。src/Portal是 Web Application 项目(有Portal.csproj),既有模块的.ascx.cs编译进bin/Portal.dll。新增模块若只在磁盘上放文件、未在 csproj 登记<Compile Include="DesktopModules\{Module}\{Module}.ascx.cs">(及.ascx.designer.cs)与<Content Include="...{Module}.ascx / module.json / Styles/*.css">,则CodeBehind在运行期报"未能加载类型 ASPNET.StarterKit.Portal.{Module}";更隐蔽的是未登记时 MSBuild 不编译这些文件,于是out var、long→string一类编译错误只在运行期炸。 - 6 套正式皮肤必须补齐主题作用域规则。 只写模块自带
Styles/{Module}.css而不在src/Portal/App_Themes/<Theme>/Default.css补body.portal-theme-<规范化名> .<模块前缀类>规则,模块在多数皮肤下丢失主题化样式(AGENTS.md 硬性要求)。取值取自各皮肤既有my-work-items同族调色板,并为不支持var()的 IE9+ 提供显式回退。 - 标记层与 CSS 类名必须一致。 标记层用
form-grid/form-field/list而模块 CSS 写.grid/.field/.recent时,模块样式与主题规则会全部落空却无任何报错。以标记层(.ascx的class=与 Web Forms 控件的CssClass=)为权威,模块 CSS 与主题规则只承载布局与取色。 - 本机配置生效源是
Web.config的<appSettings>。 业务包需经Portal.ModulePackages.Enabled白名单放行;在开发机上改src/Portal/Config/appSettings.dev.json(gitignore,不入库)的Set值不落地,运行期读的是Web.config的既有<appSettings>。放行业务包时两处一并改,并确认appSettings.dev.json的档位(Portal.ModuleProfiles.Active)含该包所在档位。
上述 1–3 已由静态门禁
Test-PortalNewModuleReadiness.ps1守护(L0 层,只依赖源码,无需 IIS/DB):& 'C:\Program Files\PowerShell\7\pwsh.exe' -NoLogo -NoProfile -File dev\scripts\Test-PortalNewModuleReadiness.ps1 ` -ModuleName LeaveRequest -ModuleCssPrefix leave-request -ExpectedPackageId HIA.LeaveRequest门禁检查:R1 csproj 登记、R2
.ascx用CodeBehind、R3 6 套皮肤均含<prefix>主题规则、R4 模块 CSS 类选择符均在.ascx出现(含CssClass)。确有 JS/代码后置专属类时用-SkipClassConsistency据实豁免并在提交信息写明。
-
Inherits使用全限定类名 -
module.json字段合规,desktopEntry与resources均在包目录内 - 权限键引用
PortalPermissions.cs常量,无硬编码字面量 - 表头带
scope="col";表单标签用label for;只读字段用dl/dt/dd - 模块标题带
role="heading"与aria-level - 无 JavaScript、无内联脚本、无 DLL/可执行文件
- 业务迁移脚本命名为
PortalBiz_*.sql且不含USE [database] -
.ascx.cs与.ascx.designer.cs已登记进Portal.csproj的<Compile>(Web Application 项目,遗漏会让CodeBehind运行期报"未能加载类型") - 6 套正式皮肤的
Default.css均已补.<模块前缀类>主题作用域规则 -
Test-PortalNewModuleReadiness.ps1 -ModuleName <Module>通过(csproj / CodeBehind / 6 皮肤 / 标记与 CSS 类名一致性) -
Test-PortalBusinessModuleSmoke.ps1 -ModuleName HIA.YourModule通过 - 新增接入编排:把该模块的冒烟检查与就绪检查加入
dev/scripts/Invoke-PortalGateSuite.ps1(L1 冒烟 / L0 就绪) - 门禁套件全绿:
Invoke-PortalGateSuite.ps1