后端前端CRM人工智能AI Agent【免费下载链接】crmComp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.项目地址https://gitcode.com/gh_mirrors/crm48/crm点击查看免费下载本文聚焦 NestJS 依赖注入场景下的里氏替换原则Liskov Substitution Principle, LSP讲解为什么子类型实现类必须能够在运行时被其基类型接口/抽象类无缝替换而不破坏调用方语义。结合 Comp AI CRM开源、面向 AI Agent 的 CRMNestJS API 位于 apps/api的实际代码你将掌握如何用注入令牌injection token为接口绑定实现、如何识别测试替身mock/stub中的 LSP 违规、以及如何编写共享契约测试来验证任意实现都遵守同一份行为契约。为什么在 NestJS 中 LSP 是 CRITICAL 级实践Comp AI CRM 的代码规范中.agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md将「Honor Liskov Substitution Principle」标记为impact: HIGH与依赖注入DI类别下的其余规则di-interface-segregation、di-prefer-constructor-injection、di-use-interfaces-tokens、di-scope-awareness、di-avoid-service-locator一起共同构成 NestJS 应用架构中 CRITICAL 级别的质量门槛。LSP 的经典定义是子类型必须能够替换其基类型而不改变程序的正确性。映射到 NestJS 依赖注入语境中规则文件给出了更精确的表述Any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.也就是说只要某段代码依赖的是接口PaymentGateway那么运行时无论容器注入StripeService还是MockPaymentService调用方的行为都必须一致。测试中使用的 mock 服务必须与生产实现返回相同形状的数据、以相同方式处理错误否则在「切换实现」的那一刻就会引入难以排查的隐性 bug。在依赖注入系统中违反 LSP 的典型后果是单元测试全绿因为测试替身放水但一旦把生产实现换进来或反过来用 mock 做 E2E立刻出现undefined字段、意外抛错、错误的异常类型等「只在特定环境出现」的问题。前提接口无法直接作为注入令牌讨论 LSP 之前必须建立基础TypeScript 接口在编译期会被擦除运行时不存在PaymentGateway这个值因此接口本身不能作为注入令牌。同类别规则 di-use-interfaces-tokens.md 明确指出TypeScript interfaces are erased at compile time and cant be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces.Comp AI CRM 正是这么做的。apps/api/src/database/database.constants.ts使用Symbol 令牌暴露数据库连接import { Inject } from nestjs/common; export const DATABASE Symbol(DATABASE); export const InjectDatabase () Inject(DATABASE);然后在 database.module.ts 中把Db实例绑定到该令牌并导出Global() Module({ providers: [{ provide: DATABASE, useValue: db }], exports: [DATABASE], }) export class DatabaseModule implements OnModuleInit, OnApplicationShutdown { constructor(InjectDatabase() private readonly db: Db) {} async onModuleInit(): Promisevoid { try { await this.db.$connect(); this.logger.log({ message: Database connected }); } catch (error) { // ... } } // ... }任意 Service 通过自定义装饰器InjectDatabase()注入例如 activities.service.ts、agent-access.service.ts、agent-definitions.service.ts、agent-queue.service.ts、agent-runs.service.ts 等都以InjectDatabase() private readonly db: Db作为构造参数。Db类型来自 packages/db/src/client.tsPrisma Client它就是一个跨实现SQLite/PostgreSQL 等保持同一行为契约的典型「实现可替换」对象。在这个基础上LSP 的完整落地需要三步用令牌定义契约 → 所有实现忠实履行契约 → 用共享契约测试验证。违规示例测试替身悄悄破坏契约规则文档给出了一个非常经典的违规示例。先看契约// Base interface with clear contract interface PaymentGateway { /** * Charges the specified amount. * returns PaymentResult on success * throws PaymentFailedException on payment failure */ charge(amount: number, currency: string): PromisePaymentResult; }生产实现遵守契约Injectable() export class StripeService implements PaymentGateway { async charge(amount: number, currency: string): PromisePaymentResult { const response await this.stripe.charges.create({ amount, currency }); return { success: true, transactionId: response.id, amount }; } }而下面这个 mock 则同时踩中三类 LSP 违规Injectable() export class MockPaymentService implements PaymentGateway { async charge(amount: number, currency: string): PromisePaymentResult { // VIOLATION 1: Throws for valid input (contract says return PaymentResult) if (amount 1000) { throw new Error(Mock does not support large amounts); } // VIOLATION 2: Returns null instead of PaymentResult if (currency ! USD) { return null as any; // Real service would convert or reject properly } // VIOLATION 3: Missing required field return { success: true } as PaymentResult; // Missing transactionId! } }三种违规类型值得单独拆解对合法输入抛错契约声明charge对合法参数返回PromisePaymentResultmock 却对amount 1000抛出普通Error。若调用方仅按契约捕获PaymentFailedException这个错误会直接冒泡成 500。返回null契约返回PaymentResultmock 对非 USD 货币返回null as any。调用方sendReceipt(result)会收到null导致 NPE/TypeError。缺少必需字段{ success: true } as PaymentResult缺少transactionId。调用方saveTransaction(result.transactionId)拿到undefined写入数据库时静默产生脏数据——这正是规则文档强调的「subtle bugs」。调用方完全信任契约Injectable() export class OrdersService { constructor(Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {} async checkout(order: Order): Promisevoid { const result await this.payment.charge(order.total, order.currency); // These fail with MockPaymentService: await this.saveTransaction(result.transactionId); // undefined! await this.sendReceipt(result); // might be null! } }测试里 mock 全绿、生产环境必炸就是 LSP 被破坏的典型信号。正确示例同一契约、同一行为形状修正方案不是「删掉 mock」而是让 mock忠实复刻生产实现的行为形状。规则文档给出的正确版本值得逐行对照// Well-defined interface with documented behavior interface PaymentGateway { /** * Charges the specified amount. * param amount - Amount in smallest currency unit (cents) * param currency - ISO 4217 currency code * returns PaymentResult with transactionId, success status, and amount * throws PaymentFailedException if charge is declined * throws InvalidCurrencyException if currency is not supported */ charge(amount: number, currency: string): PromisePaymentResult; /** * Refunds a previous charge. * throws TransactionNotFoundException if transactionId is invalid */ refund(transactionId: string, amount?: number): PromiseRefundResult; }注意这里的契约升级接口注释里显式写明了参数单位amount以最小货币单位「分」计、支持的货币码、返回结构、以及每个方法的异常类型。把行为写入接口文档是 LSP 能被遵守的前提——实现者与调用方基于同一份行为规格编程。生产实现忠实履行Injectable() export class StripeService implements PaymentGateway { async charge(amount: number, currency: string): PromisePaymentResult { try { const response await this.stripe.charges.create({ amount, currency }); return { success: true, transactionId: response.id, amount: response.amount, }; } catch (error) { if (error.type card_error) { throw new PaymentFailedException(error.message); } throw error; } } // refund(...) 实现略 }mock 遵循同一行为形状校验货币时抛InvalidCurrencyException与生产一致而非直接null、模拟特定金额的拒付、返回包含全部必需字段的PaymentResult并且实现了完整的refund契约事务不存在时抛TransactionNotFoundExceptionInjectable() export class MockPaymentService implements PaymentGateway { private transactions new Mapstring, PaymentResult(); async charge(amount: number, currency: string): PromisePaymentResult { // Honor the contract: validate currency like real service would if (![USD, EUR, GBP].includes(currency)) { throw new InvalidCurrencyException(Unsupported currency: ${currency}); } // Simulate decline for specific test scenarios if (amount 99999) { throw new PaymentFailedException(Card declined (test scenario)); } // Return same shape as production const result: PaymentResult { success: true, transactionId: mock_${Date.now()}_${Math.random().toString(36)}, amount, }; this.transactions.set(result.transactionId, result); return result; } async refund(transactionId: string, amount?: number): PromiseRefundResult { // Honor the contract: throw if transaction not found if (!this.transactions.has(transactionId)) { throw new TransactionNotFoundException(transactionId); } return { success: true, refundId: refund_${transactionId}, amount: amount ?? this.transactions.get(transactionId)!.amount, }; } }调用方因此可以安全地在测试与生产之间切换实现Injectable() export class OrdersService { constructor(Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {} async checkout(order: Order): PromiseOrder { try { const result await this.payment.charge(order.total, order.currency); // Works with both StripeService and MockPaymentService order.transactionId result.transactionId; order.status paid; return order; } catch (error) { if (error instanceof PaymentFailedException) { order.status payment_failed; return order; } throw error; } } }注意错误分支只按契约中的异常类型分流PaymentFailedException→ 标记支付失败其余异常原样上抛。mock 若抛出普通Error就会逃出此分支——这正是上一节违规示例会在真实切换时暴露的原因。用共享契约测试固化 LSP规则文档指出仅靠「自觉」无法长期维持 LSP正确做法是编写一份所有实现都必须通过的共享测试套件// Shared test suite that any implementation must pass function testPaymentGatewayContract( createGateway: () PaymentGateway, ) { describe(PaymentGateway contract, () { let gateway: PaymentGateway; beforeEach(() { gateway createGateway(); }); it(returns PaymentResult with all required fields, async () { const result await gateway.charge(1000, USD); expect(result).toHaveProperty(success); expect(result).toHaveProperty(transactionId); expect(result).toHaveProperty(amount); expect(typeof result.transactionId).toBe(string); }); it(throws InvalidCurrencyException for unsupported currency, async () { await expect(gateway.charge(1000, INVALID)) .rejects.toThrow(InvalidCurrencyException); }); it(throws TransactionNotFoundException for invalid refund, async () { await expect(gateway.refund(nonexistent)) .rejects.toThrow(TransactionNotFoundException); }); }); } // Run against all implementations describe(StripeService, () { testPaymentGatewayContract(() new StripeService(mockStripeClient)); }); describe(MockPaymentService, () { testPaymentGatewayContract(() new MockPaymentService()); });这套「契约测试」的价值在于任何一个新实现或对现有实现的改动都必须先通过这份公共断言违规会在 PR 阶段而不是生产故障中被拦截。这正是 test-mock-external-services.md 所强调的方向——mock 不是「随便写个假实现」而是「行为形状与真实服务一致、且覆盖超时与错误边界」的替身。仓库中的真实印证以结果联合类型收敛行为形状Comp AI CRM 里有一个比「示例代码」更能说明 LSP 的实战案例MailboxApiClient。它不通过抛异常表达失败而是把所有可能的响应收敛为一个可判别联合discriminated union任何调用方拿到的都是同一种行为形状。mailbox-api.client.ts 定义的返回类型export type MailboxResultT | { outcome: ok; data: T } | { outcome: cursor-invalid; reason: string } | { outcome: unauthorized; reason: string } | { outcome: rate-limited; reason: string; retryAfterMs: number } | { outcome: failed; reason: string; retryable: boolean };内部对fetch的每个分支都映射到该联合的某个成员401 → unauthorized、404/410 → cursor-invalid、403命中 rate/quota 关键词与429 → rate-limited并附带retryAfterMs退避时间、其余状态 →failedstatus 500时retryable: true。无论底层是 Gmail 还是 Google Calendar无论网络超时还是 HTTP 错误返回结构永远一致——这就是「实现可替换而不破坏调用方」的工程化表达。对应测试 mailbox-api-client.spec.ts 用globalThis.fetch打桩逐条验证契约行为200→{ outcome: ok, data: { ok: true } }Gmail 的404→cursor-invalidCalendar 的410→cursor-invalid401→unauthorized以便调用方标记行需要重新连接quota403→rate-limited可重试、带退避时间权限403→failed终态、不可重试这些断言本质上就是上面「共享契约测试」思想的单实现落地契约被写成测试行为形状被锁定。当调用方如同步服务拿到MailboxResult时它可以放心做穷举式switch而不用担心某个实现偷偷返回null或抛出意想不到的异常类型。与相邻规则配合完整落实 LSP 的检查清单LSP 不是孤立规则。在 NestJS 中让它真正生效需要与依赖注入类别下的兄弟规则协同配套规则与 LSP 的关系仓库证据di-use-interfaces-tokens接口在编译期被擦除必须用 Symbol/字符串令牌或抽象类做注入令牌否则「可替换实现」无从谈起database.constants.ts 的DATABASE Symbol(DATABASE)与InjectDatabase()di-prefer-constructor-injection构造器注入让「换实现」只发生在容器装配处调用方代码零改动LSP 收益最大化仓库各 Service 均以构造器InjectDatabase()/Inject(XxxService)注入di-interface-segregationISP接口越窄、行为约定越少实现越容易完全履约宽接口是 LSP 违约的高发区di-interface-segregation.mddi-scope-awareness若实现方与注入方作用域singleton/request/transient不一致替换后状态行为会漂移di-scope-awareness.mdtest-mock-external-services测试替身必须与真实服务「同形状、同错误处理」正是 LSP 在测试层的落地mailbox-api-client.spec.ts 的 fetch 打桩与边界断言useFactory动态装配是 LSP 在实际 DI 容器中的经典场景Comp AI CRM 的 cache.module.ts 根据REDIS_URL是否存在在工厂函数里选择 Redis 存储或进程内内存缓存——两个存储实现对外暴露的CacheOptions行为契约一致调用方Inject(CACHE_MANAGER)见 auth.service.ts无感知切换。这就是「子类型可替换而不改变程序正确性」在生产代码中的直接体现。小结LSP 在 NestJS 依赖注入中的落地可以浓缩为四条可执行准则先定义契约再写实现接口/抽象类的 JSDoc 必须写明参数单位、返回结构、异常类型没有文档化行为约定的接口实现方必然各写各的。测试替身必须与生产实现同形状返回相同的字段集、抛出相同的异常类型、对非法输入做相同的校验。mock 不是「简化版」而是「行为一致版」。用共享契约测试锁定行为让所有实现跑同一份断言套件任何新增/修改实现都必须先通过契约测试。用令牌与工厂装配实现可替换性Symbol/字符串令牌 useClass/useFactory动态选择实现让「替换」发生在容器装配处调用方零改动。从 di-liskov-substitution.md 的支付网关示例到 Comp AI CRM 中InjectDatabase()的 Symbol 令牌、MailboxApiClient的可判别联合返回、以及AppCacheModule的useFactory双存储切换LSP 的本质始终如一让「实现可以互换」成为系统的默认属性而不是一次冒险的替换操作。赞分享后端前端CRM人工智能AI Agent【免费下载链接】crmComp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.项目地址https://gitcode.com/gh_mirrors/crm48/crm点击查看免费下载相关推荐终极指南macOS上如何彻底阻止iTunes和Apple Music自动启动终极指南macOS上如何彻底阻止iTunes和Apple Music自动启动 你是否经常被macOS上iTunes或Apple Music的自动启动打断工作流桌面应用如何掌握PHP里氏替换原则clean-code-php中的LSP实践指南如何掌握PHP里氏替换原则clean code php中的LSP实践指南 clean code php 是一个专注于将Clean Code概念应用于PHP开发教程rsschool-app 后端中的 Liskov 替换原则NestJS 依赖注入下的可替换实现设计rsschool app 后端中的 Liskov 替换原则NestJS 依赖注入下的可替换实现设计 本文基于本仓库 .agents/skills/nestjs教育后端前端上一篇Backstage v1.33.0 发布解读目录性能优化与面包屑导航、只读文件系统配置注入、Scaffolder Node.js 22 支持等关键更新下一篇Semantic Kernel 中的 AWS Bedrock Agent 集成从环境配置到多 Agent 编排实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
PyCaret 4.0 引擎开发协作指南:OOP-only 公共 API、类型化结果与事件日志规范 【免费下载链接】pycaret Open-source, low-code AutoML platform for Python. PyCaret 4.0: sklearn-native engine React control plane. 项目地址: https://gitcode.com/gh_mirrors/py/pycaret 点击查看 免费下载 PyCaret 4.0 将引擎重构为"瘦身、无状态、… · 2026/9/24 17:22:01
OpenChamber 从已验证 Issue 到最小修复:/bug-work 命令驱动的 Bug 工作流全解析 OpenChamber 从已验证 Issue 到最小修复:/bug-work 命令驱动的 Bug 工作流全解析 【免费下载链接】openchamber Agentic Development Environment based on OpenCode AI agent 项目地址: https://gitcode.com/gh_mirrors/op/openchamber
OpenChamber 是一个基… · 2026/9/24 17:21:55
基于粒子群算法的光伏MPPT控制Simulink仿真实现 手头有做光伏发电控制的朋友,应该都懂MPPT这三个字的含金量。传统的扰动观察法、电导增量法在光照均匀时都很能打,但一旦组件被云朵、建筑物、落叶遮住半边,P-V曲线出现多峰,这批“单峰猎人”就全抓瞎了,系统可能直接锁… · 2026/9/24 21:12:26
音频压缩6个方法详解:从MP3到Opus,有损无损一次讲透 打开你的手机看看,是不是光一个微信就吃掉了十几个G,其中语音文件、视频聊天记录、下载的音乐占了一大半。再把目光转向电脑,录一段播客、剪一条片子,随手导出的音频动不动就是几百MB,发个邮件都提示附件过大。这些都是… · 2026/9/24 21:12:26
基于SpringBoot+Vue的师生健康信息管理系统设计与实现全解析 每年到毕设季,找我要选题建议的同学里,十有八九会问“有没有那种功能完整、技术栈主流、还不太容易翻车的题目”,而“师生健康信息管理系统”就是我从头到尾都很推荐的一类。原因也很简单:这个题目管理的数据对象明确、角色分工清… · 2026/9/24 21:12:26
工厂焊装车间照明节能改造:KNX照明系统方案分区灯控人体感应 焊装车间是汽车工厂中照明设计最复杂的场景之一。焊接作业时弧光强烈,而检验工位又要求极高照度——两者对灯光的需求完全不同,用同一套照明方案无法兼顾。据《乘用车工厂焊装车间照明节能设计的探讨》一文披露,一汽大众华北生产基地焊装车间… · 2026/9/24 21:12:19
Mac 上如何替代 Notepad++:兼容层、原生编辑器与命令行实践 简介:这份文档面向希望在 Mac 电脑上使用 Notepad 的用户,尤其是习惯 Windows 编辑环境、又不愿更换工具的开发者与运维人员。由于 Notepad 官方并未推出 Mac 版本,资源围绕借助 WineBottler 在 macOS 上运行 Windows 程序的思路展开… · 2026/9/24 21:12:19
Java SpringBoot Vue3全栈商城系统设计与实现解析 这一套「Java SpringBoot Vue3 MyBatis MySQL」的在线商城系统源码,算是这几年Java后端很主流、也最适合练手的一类全栈项目了。前后端分离、RESTful接口、JWT鉴权、商品订单流转、后台管理,覆盖了一个中型Web系统的大部分核心知识点。不管是拿来做毕… · 2026/9/24 21:12:19
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44