新闻详情

新闻详情

首页 / 资讯中心 / 详情

在 NestJS 中集成 MikroORM:模块初始化、请求上下文、多数据库与测试完整指南

发布时间:2026/9/25 5:58:24来源:尧图网络
在 NestJS 中集成 MikroORM:模块初始化、请求上下文、多数据库与测试完整指南
后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载本文基于 MikroORM 5.9 版本官方文档《Using MikroORM with NestJS framework》编写系统讲解如何通过mikro-orm/nestjs将基于 Data Mapper、Unit of Work 与 Identity Map 模式的 TypeScript ORM 集成进 NestJS 应用。你将掌握MikroOrmModule.forRoot()/forFeature()的初始化与仓库注册方式、队列与定时任务中的UseRequestContext()请求上下文隔离、多数据库连接contextName管理、GraphQL 与序列化陷阱以及基于getRepositoryToken()的单元测试 Mock 策略并结合本仓库源码理解其底层实现。一、安装与驱动选择集成 MikroORM 到 NestJS 最便捷的方式是使用mikro-orm/nestjs模块。安装时需同时安装 NestJS、MikroORM 核心包以及你实际使用的数据库驱动包# yarn $ yarn add mikro-orm/core mikro-orm/nestjs mikro-orm/mongodb # for mongo $ yarn add mikro-orm/core mikro-orm/nestjs mikro-orm/mysql # for mysql/mariadb $ yarn add mikro-orm/core mikro-orm/nestjs mikro-orm/mariadb # for mysql/mariadb $ yarn add mikro-orm/core mikro-orm/nestjs mikro-orm/postgresql # for postgresql $ yarn add mikro-orm/core mikro-orm/nestjs mikro-orm/sqlite # for sqlite# npm $ npm i -s mikro-orm/core mikro-orm/nestjs mikro-orm/mongodb # for mongo $ npm i -s mikro-orm/core mikro-orm/nestjs mikro-orm/mysql # for mysql/mariadb $ npm i -s mikro-orm/core mikro-orm/nestjs mikro-orm/mariadb # for mysql/mariadb $ npm i -s mikro-orm/core mikro-orm/nestjs mikro-orm/postgresql # for postgresql $ npm i -s mikro-orm/core mikro-orm/nestjs mikro-orm/sqlite # for sqlite需要说明的是驱动包必须与目标数据库一一对应mikro-orm/mysql同时覆盖 MySQL 与 MariaDB也可直接选择mikro-orm/mariadb。仓库中每个驱动包都位于独立的 packages 目录下例如 packages/mongodb、packages/mysql、packages/postgresql、packages/sqlite它们共享 packages/core 的核心实现。二、通过 MikroOrmModule.forRoot() 初始化安装完成后在根AppModule中导入MikroOrmModule并调用静态方法forRoot()Module({ imports: [ MikroOrmModule.forRoot({ entities: [./dist/entities], entitiesTs: [./src/entities], dbName: my-db-name.sqlite3, type: sqlite, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}forRoot()接受与 MikroORM 包中init()完全相同的配置对象完整配置项说明见 configuration.md。这里常用的几个关键字段type数据库类型如sqlite、mysql、mariadb、postgresql、mongodb、mssqlentities编译后dist实体文件的 glob 路径entitiesTsTypeScript 源码src实体文件的 glob 路径供 CLI 在 TS 环境下使用dbName数据库名对 SQLite 而言即文件名如my-db-name.sqlite3。从仓库源码看配置解析集中在 packages/core/src/utils/Configuration.ts 中。该文件第 51 行起定义了DEFAULTS默认值对象例如contextName: default、context: (name: string) RequestContext.getEntityManager(name)、allowGlobalContext: false等forRoot()传入的配置最终会与这些默认值合并并经过校验。2.1 复用 CLI 配置文件除了直接传配置对象还可以创建mikro-orm.config.ts配置文件然后不带任何参数调用forRoot()Module({ imports: [ MikroOrmModule.forRoot(), ], ... }) export class AppModule {}这样 NestJS 会读取与 CLI 配置 相同的配置文件实现「一套配置ORM 与 CLI 共用」。注意当你使用会进行 tree shaking 的构建工具如 webpack时这种写法可能无法正常工作此时应显式传入配置对象。三、注入 MikroORM 与 EntityManager初始化完成后EntityManager可以在整个项目中直接注入无需在其他模块重复导入import { MikroORM } from mikro-orm/core; import { EntityManager } from mikro-orm/mysql; // 从你的驱动包或 mikro-orm/knex 导入 EntityManager Injectable() export class MyService { constructor(private readonly orm: MikroORM, private readonly em: EntityManager) { } }注意EntityManager需从你正在使用的驱动包导入如mysql、sqlite、postgres、mongodb等。如果项目中安装了mikro-orm/knex依赖也可以从那里导入。之所以能从驱动包导出EntityManager是因为各驱动包在 packages/mysql/src 等目录中统一 re-export 核心类型。从源码看EntityManager的构造函数位于 packages/core/src/EntityManager.ts它会读取config.get(contextName)作为自身name属性而该名字正是请求上下文中按数据库连接区分EntityManager的依据详见第六节。四、Repository 模式与 forFeature()MikroORM 支持 Repository 设计模式每个实体都可以对应一个仓库类完整文档见 repositories.md。在 NestJS 中使用forFeature()方法把仓库注册到当前模块作用域注意不要通过forFeature()注册你的基类实体base entities因为基类没有对应的仓库但基类必须出现在forRoot()的实体列表中或 ORM 全局配置中。// photo.module.ts Module({ imports: [MikroOrmModule.forFeature([Photo])], providers: [PhotoService], controllers: [PhotoController], }) export class PhotoModule {}然后在根AppModule中导入该模块// app.module.ts Module({ imports: [MikroOrmModule.forRoot(...), PhotoModule], }) export class AppModule {}这样便可以在PhotoService中通过InjectRepository()装饰器注入PhotoRepositoryInjectable() export class PhotoService { constructor( InjectRepository(Photo) private readonly photoRepository: EntityRepositoryPhoto ) {} // ... }五、自定义 Repository 与依赖注入令牌使用自定义仓库时只要仓库命名与getRepositoryToken()的返回规则一致就可以省去InjectRepository()装饰器。该函数的生成规则如下export const getRepositoryToken T (entity: EntityNameT) ${Utils.className(entity)}Repository;也就是说只要仓库名是「实体名 Repository后缀」它就会被自动注册到 NestJS 的 DI 容器中。实体侧通过Entity({ customRepository: ... })声明自定义仓库并配合[EntityRepositoryType]让em.getRepository()具备类型推断能力// ./author.entity.ts Entity({ customRepository: () AuthorRepository }) export class Author { // 让 em.getRepository() 能够推断出返回类型 [EntityRepositoryType]?: AuthorRepository; }// ./author.repository.ts import { EntityRepository } from mikro-orm/mysql; // 从驱动包或 mikro-orm/knex 导入 export class AuthorRepository extends EntityRepositoryAuthor { // your custom methods... }由于自定义仓库名与getRepositoryToken()返回的令牌一致服务中可直接按类型注入Injectable() export class MyService { constructor(private readonly repo: AuthorRepository) { } }从仓库源码看em.getRepository()的实现位于 packages/core/src/EntityManager.ts它会通过metadata.get(entityName)获取元数据再通过this.config.getRepositoryClass(meta.repository)解析customRepository指定的仓库类最后把实例缓存在#repositoryMap中确保同一实体只创建一份仓库实例。六、autoLoadEntities自动加载实体autoLoadEntities选项在 v4.1.0 中引入。手动把实体逐一加入连接配置的entities数组很繁琐而且在根模块引用实体还会破坏应用的领域边界、把实现细节泄漏到其他模块。为此可以使用静态 glob 路径但webpack 不支持 glob 路径在 monorepo 场景下无法使用。替代方案是开启autoLoadEntitiesModule({ imports: [ MikroOrmModule.forRoot({ ... autoLoadEntities: true, }), ], }) export class AppModule {}开启后所有通过forFeature()注册的实体都会被自动追加到配置对象的entities数组中。注意仅被其他实体通过关系引用、但本身未经过forFeature()注册的实体不会被autoLoadEntities包含仍需显式注册。另外autoLoadEntities对 MikroORM CLI没有效果——CLI 仍需要包含完整实体列表的 CLI 配置不过在 CLI 配置中可以使用 glob因为 CLI 不经过 webpack。七、队列与定时任务中的请求作用域UseRequestContext()UseRequestContext()装饰器在 v4.1.0 中引入自 v5 起它可以直接从mikro-orm/core包导入不再局限于 NestJS 项目。如 identity-map.md 所述每个请求都需要干净的clean上下文状态这在普通 HTTP 请求中由RequestContext中间件自动处理。但中间件只在常规 HTTP 请求处理器中生效——对于队列处理器、定时任务这类脱离 HTTP 请求的场景就需要手动创建请求上下文。使用UseRequestContext()即可它要求先把MikroORM实例注入到当前上下文装饰器会在底层为你的方法注册一个新的请求上下文并在该上下文中执行方法体UseRequestContext()只能用于顶层方法不能嵌套使用——被它装饰的方法不应再调用另一个同样被它装饰的方法。Controller() export class MyService { constructor(private readonly orm: MikroORM) { } UseRequestContext() async doSomething() { // 此方法将在独立上下文中执行 } }也可以提供一个返回MikroORM实例的回调函数import { DI } from ..; export class MyService { UseRequestContext(() DI.orm) async doSomething() { // 此方法将在独立上下文中执行 } }7.1 与 BullJS 队列装饰器的组合技巧使用UseRequestContext()时要注意它与其他装饰器的组合顺序。例如与 NestJS 的 BullJS 队列模块一起使用时安全做法是把需要干净上下文的代码段单独抽成方法或注入到独立服务中Processor({ name: example-queue, }) export class MyConsumer { constructor(private readonly orm: MikroORM) { } Process() async doSomething(job: Jobany) { await this.doSomethingWithMikro(); } UseRequestContext() async doSomethingWithMikro() { // 此方法将在独立上下文中执行 } }原因在于Process()期望收到一个可执行的函数而如果把UseRequestContext()也加到被Process()装饰的方法上且UseRequestContext()先于Process()执行Process()收到的将是void导致队列处理失效。7.2 底层实现RequestContext 与 AsyncLocalStorage要理解这个装饰器做了什么可以看仓库中 packages/core/src/utils/RequestContext.ts 的实现。自 v5 起RequestContext使用 Node.js 的AsyncLocalStorage保存异步上下文static create(em, next, options)把传入的EntityManager或EntityManager[]通过em.fork({ useContext: true })派生为上下文专属 fork再调用this.storage.run(ctx, next)在隔离的异步存储中执行回调支持多连接时传入EntityManager[]内部会构建一个Mapstring, EntityManager键为各 EM 的name即contextNamestatic getEntityManager(name default)从当前上下文的 Map 中取出对应名字的 EMstatic currentRequestContext()通过this.storage.getStore()取回当前上下文。AsyncLocalStorage的创建封装在 packages/core/src/utils/AsyncContext.ts 中优先使用全局AsyncLocalStorage其次通过node:async_hooks获取实在不可用时回退到一个简单的全局变量实现并打印警告。这些机制共同保证了「每次请求一个干净 EM fork」的隔离语义也正是UseRequestContext()能在队列、定时任务等非 HTTP 场景下复用的根本原因。顺带说明在本文对应的 5.9 版本中该装饰器名为UseRequestContext()从mikro-orm/core导入从当前仓库源码看后续版本中它演进为CreateRequestContext()并迁移至mikro-orm/decorators包相关实现见 packages/decorators/src/legacy/CreateRequestContext.ts。八、GraphQL 场景下的请求上下文处理NestJS 的 GraphQL 模块基于apollo-server-express默认开启bodyparser。这会造成问题NestJS MikroORM 模块安装的中间件必须在bodyparser之后加载否则请求上下文无法正确建立详见 identity-map.md 中「RequestContext helper for DI containers」一节。同时要确保在 NestJS 侧禁用 body-parser。在main.ts中显式加入 body-parserimport { NestFactory } from nestjs/core; import express from express; async function bootstrap() { const app await NestFactory.create(AppModule,{ bodyParser: false }); app.use(express.json()); await app.listen(5555); }同时在 GraphQL 模块中禁用其内置 bodyparserModule({ imports: [ GraphQLModule.forRoot({ bodyParserConfig: false, }), ], })九、应用关闭与资源清理默认情况下NestJS 不监听系统进程终止信号如SIGTERM。因此如果进程被直接终止MikroORM 的关闭逻辑永远不会执行可能导致数据库连接一直保持打开、持续占用资源。要解决这个问题需要在启动应用时调用enableShutdownHooksasync function bootstrap() { const app await NestFactory.create(AppModule); // 开始监听关闭钩子 app.enableShutdownHooks(); await app.listen(3000); }关于enableShutdownHooks的更多信息可参考 NestJS 生命周期事件文档中的 Application shutdown 一节。启用后进程收到终止信号时会触发应用关闭流程MikroORM 借此机会释放连接池等资源。十、多数据库连接contextName 与注入令牌可以通过注册多个MikroOrmModule并分别设置contextName来定义多个数据库连接。如果希望使用中间件请求上下文必须禁用自动中间件并通过forMiddleware()注册请求上下文中间件或改用 NestJS 的 Injection ScopeModule({ imports: [ MikroOrmModule.forRoot({ contextName: db1, registerRequestContext: false, // 禁用自动中间件 ... }), MikroOrmModule.forRoot({ contextName: db2, registerRequestContext: false, // 禁用自动中间件 ... }), MikroOrmModule.forMiddleware() ], controllers: [AppController], providers: [AppService], }) export class AppModule {}访问不同连接时必须使用新的注入令牌InjectMikroORM()/InjectEntityManager()并传入contextNameInjectable() export class MyService { constructor(InjectMikroORM(db1) private readonly orm1: MikroORM, InjectMikroORM(db2) private readonly orm2: MikroORM, InjectEntityManager(db1) private readonly em1: EntityManager, InjectEntityManager(db2) private readonly em2: EntityManager) { } }用forFeature()定义仓库时需要指定要注册到的contextName// photo.module.ts Module({ imports: [MikroOrmModule.forFeature([Photo], db1)], providers: [PhotoService], controllers: [PhotoController], }) export class PhotoModule {}使用InjectRepository装饰器时同样要传入contextNameInjectable() export class PhotoService { constructor( InjectRepository(Photo, db1) private readonly photoRepository: EntityRepositoryPhoto ) {} // ... }10.1 底层依据name 与上下文 Map 的对应关系从源码可以印证这套多连接机制的设计EntityManager构造函数会把配置中的contextName赋给自己的name属性packages/core/src/EntityManager.ts而RequestContext内部正是用Mapstring, EntityManager按 name 存储各连接的 forkpackages/core/src/utils/RequestContext.ts。InjectMikroORM(db1)/InjectEntityManager(db1)本质上就是告诉 DI 容器与请求上下文「我要拿 name 为 db1 的那份实例」。此外 packages/core/src/utils/Configuration.ts 中contextName的默认值为defaultcontext默认回调即RequestContext.getEntityManager(name)——这也解释了为何单连接场景下无需显式传参。十一、序列化注意事项NestJS 内置序列化依赖 class-transformer。由于 MikroORM 出于类型安全考虑把每个实体关系都包装在Reference或Collection实例中这会让内置的ClassSerializerInterceptor对包装后的关系「视而不见」。换言之如果从 HTTP 或 WebSocket 处理器直接返回 MikroORM 实体其所有关系都不会被序列化。解决方法是使用 MikroORM 自带的序列化 API详见 serializing.md它提供了与 class-transformer 各装饰器一一对应的能力Entity() export class Book { Property({ hidden: true }) // -- 等价于 class-transformer 的 Exclude hiddenField: number Date.now(); Property({ persist: false }) // -- 仅存在于内存且会被序列化类似于 class-transformer 的 Expose() count?: number; ManyToOne({ serializer: value value.name, serializedName: authorName }) // 等价于 class-transformer 的 Transform() author: Author; }hidden: true序列化时排除该字段persist: false字段不持久化到数据库仅在内存中存在且会被序列化serializerserializedName自定义字段的序列化输出与输出字段名。十二、单元测试中的 Mock 策略mikro-orm/nestjs包暴露了getRepositoryToken()函数它根据给定实体返回预生成的令牌方便对仓库进行 Mock如果只通过getRepositoryToken()注册 provider就必须配合InjectRepository装饰器注入若想在不使用该装饰器的情况下注入自定义仓库则需要以provide: PhotoRepository的形式注册。Module({ providers: [ PhotoService, // 供 InjectRepository 装饰器使用 { provide: getRepositoryToken(Photo), useValue: mockedRepository, }, // 自定义仓库在不使用 InjectRepository 时所需 { provide: PhotoRepository, useValue: mockedRepository, }, ], }) export class PhotoModule {}第一种写法通过令牌getRepositoryToken(Photo)覆盖 DI 容器中该实体的仓库绑定InjectRepository(Photo)注入时取到的是mockedRepository第二种写法直接按自定义仓库类名PhotoRepository覆盖绑定因此constructor(private readonly repo: PhotoRepository)也能拿到 Mock 实例。两种方式可以按测试场景组合使用。十三、基于 AsyncLocalStorage 的自定义请求上下文历史说明:::info 自 v5 起RequestContext内部已使用AsyncLocalStorage因此本节内容在 v5 中已不再适用仅作为旧版本v4 早期的兼容写法保留参考。 :::在旧版本中RequestContext使用 Node.js 的domainAPI。自mikro-orm/core4.0.3起如果 Node 版本足够新也可以改用AsyncLocalStorage// 创建新的全局存储实例 const storage new AsyncLocalStorageEntityManager(); Module({ imports: [ MikroOrmModule.forRoot({ // ... registerRequestContext: false, // 禁用自动中间件 context: () storage.getStore(), // 使用我们自己的 AsyncLocalStorage 实例 }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} // 注册请求上下文中间件 const app await NestFactory.create(AppModule, { ... }); const orm app.get(MikroORM); app.use((req, res, next) { storage.run(orm.em.fork({ useContext: true }), next); });这里的context配置项在 packages/core/src/utils/Configuration.ts 中被类型化为(name: string) EntityManager | undefined即「按 contextName 返回当前请求上下文的 EM」允许用户完全接管上下文的获取方式。十四、在 NestJS 中使用 EventSubscriber在 NestJS 中使用事件订阅器时应使用Injectable并手动注册订阅器而不是使用Subscriber装饰器。典型做法是在构造函数中拿到EntityManager通过其getEventManager().registerSubscriber(this)完成注册import { Injectable } from nestjs/common; import { EntityName, EventArgs, EventSubscriber } from mikro-orm/core; Injectable() export class AuthorSubscriber implements EventSubscriberAuthor { constructor(em: EntityManager) { em.getEventManager().registerSubscriber(this); } getSubscribedEntities(): EntityNameAuthor[] { return [Author]; } async afterCreate(args: EventArgsAuthor): Promisevoid { // ... } async afterUpdate(args: EventArgsAuthor): Promisevoid { // ... } }getSubscribedEntities()声明该订阅器关心的实体afterCreate/afterUpdate等钩子在对应生命周期事件发生时由事件管理器回调。这样既能利用 NestJS 的依赖注入管理订阅器生命周期又能完整接入 MikroORM 的事件系统。十五、小结本文完整覆盖了在 NestJS 中使用 MikroORM 5.9 的九大实战场景初始化forRoot()直接传配置或复用mikro-orm.config.ts注意 tree shaking 限制注入全局注入MikroORM与驱动包导出的EntityManager仓库forFeature()注册 InjectRepository()注入自定义仓库借助getRepositoryToken()的命名规则免装饰器注入实体自动加载autoLoadEntities: true把forFeature()注册的实体自动并入entities注意对 CLI 与关系引用实体的边界非 HTTP 场景队列/定时任务中用UseRequestContext()隔离请求上下文底层由RequestContextAsyncLocalStorage的fork({ useContext: true })机制支撑GraphQL统一 body-parser 加载顺序避免请求上下文失效关闭清理enableShutdownHooks()保证进程终止时释放数据库连接多数据库contextNameforMiddleware()InjectMikroORM()/InjectEntityManager()/InjectRepository(token, contextName)组合测试与序列化getRepositoryToken()Mock 策略以及用 MikroORM 序列化 API 替代ClassSerializerInterceptor。每个场景既给出了可直接复制的代码也提供了本仓库中的源码证据RequestContext、AsyncLocalStorage封装、contextName默认值与EntityManager命名机制帮助你从「会配置」进阶到「懂原理」。如果你希望进一步研究实体定义、关系映射或 Identity Map 语义可继续阅读本仓库的 defining-entities.md、relationships.md 与 identity-map.md。赞分享后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐在 NestJS 中集成 MikroORM模块化配置、请求上下文与多数据库实战指南在 NestJS 中集成 MikroORM模块化配置、请求上下文与多数据库实战指南 本篇指南以 mikro orm/nestjs 模块为核心系统讲解如何在后端MikroORM 与 NestJS 集成实战MikroOrmModule、仓储注入与请求上下文管理MikroORM 与 NestJS 集成实战MikroOrmModule、仓储注入与请求上下文管理 在 NestJS 应用中集成 MikroORM最顺畅的方后端MikroORM与PHPUnit集成编写可靠数据库测试的完整指南MikroORM与PHPUnit集成编写可靠数据库测试的完整指南 引言 在现代PHP应用开发中数据库测试是确保应用数据操作正确性的关键环节。MikroORM后端上一篇Divinity Mod Manager神界原罪2模组管理的终极解决方案下一篇x402 MCP Client 实战为 MCP 工具调用接入自动支付TypeScript 示例全解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

WS2812B灯环实战:Arduino流水灯与彩虹灯环完整指南 2026/9/25 6:29:11

WS2812B灯环实战:Arduino流水灯与彩虹灯环完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Allegro到立创EDA专业版:PCB导入转换的实操指南 2026/9/25 6:29:11

Allegro到立创EDA专业版:PCB导入转换的实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
中科蓝讯LB2002定制RV32工具链深度解析 2026/9/25 6:29:11

中科蓝讯LB2002定制RV32工具链深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Jetson Orin Nano Super深度评测:249美元边缘AI工作站实战解析 2026/9/25 6:29:11

Jetson Orin Nano Super深度评测:249美元边缘AI工作站实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
微信小程序课堂签到系统:四种签到模式与SSM后台实现 2026/9/25 6:29:11

微信小程序课堂签到系统:四种签到模式与SSM后台实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
计及光伏逆变器快速无功响应的分布式电源优化配置 2026/9/25 6:29:05

计及光伏逆变器快速无功响应的分布式电源优化配置

1. 为什么传统分布式电源配置方案会“浪费”光伏的无功价值?先抛个实际场景。你在做一个园区或县域配电网的分布式电源规划,手里拿着几个光伏电站的接入申请,业主要求在满足电压质量的前提下尽量多装容量。于是你按常规思路建了个优化模型&am…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉