.Net中的AOP系列之构建一个汽车租赁应用
这一节会构建一个汽车租赁系统先是给定业务需求然后逐渐地添加代码来满足那些需求。一开始不使用任何AOP从零开始敲代码。业务需求是最重要的因此我们先做需求一旦满足了业务逻辑然后再覆盖非功能需求。最后尽可能地简化并重构代码不使用AOP来重构横切关注点。这些都完成之后就会转向一个应用生命周期的长尾阶段。软件很少是长期不变的新的功能需求和新发现的bugs。很少有软件的开发阶段会比生产阶段长这就意味着大多数软件的生命周期是维护阶段。一个维护困难或昂贵的应用会导致高代价或者低品质或两者都有最终形成一个大泥球。然后会使用PostSharp重构代码将各自的横切关注点分离到它们自己的类中。一旦重构完成你就会看到使用AOP的好处特别是添加更多功能时。开始一个新项目时间现在地点你公司汽车租赁服务相关的研发部的办公室人物你的技术团队或者只有你自己背景启动一个新的项目高大上一点叫做客户忠诚度系统low一点叫做客户积分程序。目的是为了增加销售奖励那些经常购买服务的客户。比如客户今天租赁了一辆车那么他就会获得积分积分累积多了之后以后可以用于抵消一部分租赁费用或其他费用。假设有一个基本的三层架构如下图。我们会从应用到这个积分系统的核心业务逻辑层着手编写代码持久化层会跟踪客户的忠诚度积分业务逻辑层供所有的UI层使用网站APP和店员使用的桌面端。图片这一篇我们主要看一下中间一层的业务逻辑层。我们可以假设持久化层已经实现了还要假设一旦业务逻辑实现了UI也就实现了。业务需求项目经理和利益相关人比如销售和市场确定了下图的业务需求你已经确定了两个主要的需求集累积积分和使用累积的积分 兑换奖励。图片现在的业务需求就是客户每租一天普通型车辆累积一积分豪华型或者大型车辆每天两积分。这些积分会在他们支付之后并返还了车以后会增加到他们的账户中。一旦客户累积了10积分那么就可以使用这些积分兑换奖励了具体兑换规则见上图。这就是所有业务规则但是在实现之前还是得和销售和市场确定好因为他们将来肯定还会更改或者添加一些东西。必要的非功能需求在给项目经理估算时间和花销之前你有自己必须要解决的技术关注点。第一需要记录日志。如果客户的积分累积得不对累积少了那么他们会生气的因此必须确保记录了业务逻辑处理的一切尤其是起初阶段。第二因为业务逻辑代码会被多个UI应用使用要确保传入业务层的数据是合法的你的队友可能会在UI里写入一些集成代码因此必须编写防御性代码来检查无意义的边缘情况和参数。第三还是因为业务逻辑代码会被多个UI应用使用这些UI可能会使用不同类型的连接缓慢的移动手机的连接国外浏览器访问等等你需要采用事务和重试逻辑来确保维护数据集成以及给用户提供一个愉快的体验。最后总有意外会发生你可能不知道此时你会使用何种类型的持久化所以需要某种方法处理异常很可能是记录日志。没有AOP的生活将评估提交给项目经理之后所有的批准和文件也已经签署了现在就可以开始了。新建一个解决方案名叫CarRental并创建一个类库项目存放业务逻辑取名CarRental.Core编写业务逻辑创建一个累积积分的接口代码如下public interface ILoyaltyAccrualService{void Accrue(RentalAgreement agreement);}RentalAgreement是该积分系统领域公用的一个实体类因此按理说它应该在一个不同的程序集但这里为了演示我创建了一个Entities的文件夹存放所有的实体。public class RentalAgreement{public Guid Id { get; set; }public Customer Customer { get; set; }public Vehicle Vehicle { get; set; }public DateTime StartDate { get; set; }public DateTime EndDate { get; set; }}public class Customer{public Guid Id { get; set; }public string Name { get; set; }public string DriversLicense { get; set; }public DateTime DateOfBirth { get; set; }}public class Vehicle{public Guid Id { get; set; }public string Make { get; set; }public string Model { get; set; }public Size Size { get; set; }public string Vin { get; set; }}public enum Size{Compact0,Midsize,FullSize,Luxury,Truck,SUV}再回头看ILoyaltyAccrualService接口该接口有一个使用了这些实体的Accure方法用来为客户累积积分。下面是该接口的实现它会依赖一个持久化数据的服务。Accure方法会包含了计算协议中天数和这些天共累积多少积分的业务逻辑并将这些积分数量存储到数据库中。public class LoyaltyAccrualService:ILoyaltyAccrualService{private readonly ILoyaltyDataService _loyaltyDataService;public LoyaltyAccrualService(ILoyaltyDataService loyaltyDataService) { _loyaltyDataService loyaltyDataService;//数据服务必须在该对象初始化时传入该对象 } /// summary /// 该方法包含了积分系统累积客户积分的逻辑和规则 /// /summary /// param nameagreement租赁协议实体/param public void Accrue(RentalAgreement agreement) { var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays*pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id,points); }}ILoyaltyDataService只有两个方法public interface ILoyaltyDataService{void AddPoints(Guid customerId,int points);void SubstractPoints(Guid customerId, int points);}ILoyaltyDataService作为数据库接口会通过DI的方式传入到业务层的构造函数。因为我们现在只集中在业务逻辑层所以我们在数据服务层只是简单地打印一些东西就好了FakeLoyaltyDataService实现了ILoyaltyDataService如下public class FakeLoyalDataService:ILoyaltyDataService{public void AddPoints(Guid customerId, int points){Console.WriteLine(“客户{0}增加了{1}积分”,customerId,points);}public void SubstractPoints(Guid customerId, int points) { Console.WriteLine(客户{0}减少了{1}积分, customerId, points); }}到这里已经完成了累积积分的业务逻辑现在回到客户关心的问题上如何兑换积分创建一个接口ILoyaltyRedemptionServicepublic interface ILoyaltyRedemptionService{void Redeem(Invoice invoice, int numberOfDays);}////// 发票实体///public class Invoice{public Guid Id { get; set; }public Customer Customer { get; set; }public Vehicle Vehicle { get; set; }public int CostPerDay { get; set; }public decimal Discount { get; set; }}兑换积分是基于客户租赁的车型和兑换的天数从客户的账户中减去积分并填充发票中的折扣金额。代码如下public class LoyalRedemptionService:ILoyaltyRedemptionService{private readonly ILoyaltyDataService _loyaltyDataService;public LoyalRedemptionService(ILoyaltyDataService loyaltyDataService) { _loyaltyDataService loyaltyDataService; } public void Redeem(Invoice invoice, int numberOfDays) { var pointsPerDay 10; if (invoice.Vehicle.SizeSize.Luxury) { pointsPerDay 15; } var totalPoints pointsPerDay*numberOfDays; invoice.Discount numberOfDays*invoice.CostPerDay; _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints); }}测试业务逻辑下面创建一个控制台UI模拟业务逻辑的使用class Program{static void Main(string[] args){SimulateAddingPoints();//模拟累积Console.WriteLine(“***************”);SimulateRemovingPoints();//模拟兑换Console.Read();}/// summary /// 模拟累积积分 /// /summary static void SimulateAddingPoints() { var dataServicenew FakeLoyalDataService();//这里使用的数据库服务是伪造的 var servicenew LoyaltyAccrualService(dataService); var agreementnew RentalAgreement { Customer new Customer { Id Guid.NewGuid(), Name tkb至简, DateOfBirth new DateTime(2000,1,1), DriversLicense 123456 }, Vehicle new Vehicle { Id Guid.NewGuid(), Make Ford, Model 金牛座, Size Size.Compact, Vin 浙-ABC123 }, StartDate DateTime.Now.AddDays(-3), EndDate DateTime.Now }; service.Accrue(agreement); } /// summary /// 模拟兑换积分 /// /summary static void SimulateRemovingPoints() { var dataService new FakeLoyalDataService(); var service new LoyalRedemptionService(dataService); var invoice new Invoice { Customer new Customer { Id Guid.NewGuid(), Name Farb, DateOfBirth new DateTime(1999, 1, 1), DriversLicense abcdef }, Vehicle new Vehicle { Id Guid.NewGuid(), Make 奥迪, Model Q7, Size Size.Compact, Vin 浙-DEF123 }, CostPerDay 100m, Id Guid.NewGuid() }; service.Redeem(invoice,3);//这里兑换3天 }}运行程序伪造的数据服务会在控制台上打印一些东西结果如下图片现在业务逻辑完成了代码很干净分离地也很好很容易阅读和维护但是这代码还不能进入生产环境因为有各种各样可能会出错的事情发生因此下面着手新功能的需求开发。添加日志虽然审计积分事务还不是一个需求但是为了安全起见最好还是记录每个请求至少是为了QA质量保证的目的。在生产环境可能会限制或减少日志但是现在我们要放一些简单的日志帮助开发者重现QA找到的bugs。现在当累积积分和兑换积分时添加日志其余代码和之前的一样。////// 该方法包含了积分系统累积客户积分的逻辑和规则////// 租赁协议实体public void Accrue(RentalAgreement agreement){Console.WriteLine(Accrue:{0},DateTime.Now); Console.WriteLine(Customer:{0},agreement.Customer.Id); Console.WriteLine(Vehicle:{0},agreement.Vehicle.Id); var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays*pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id,points); Console.WriteLine(Accrue Complete{0},DateTime.Now);}public void Redeem(Invoice invoice, int numberOfDays){Console.WriteLine(“Redeem:{0}”,DateTime.Now);Console.WriteLine(“Invoice:{0}”,invoice.Id);var pointsPerDay 10;if (invoice.Vehicle.SizeSize.Luxury){pointsPerDay 15;}var totalPoints pointsPerDaynumberOfDays;invoice.Discount numberOfDaysinvoice.CostPerDay;_loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);Console.WriteLine(“Redeem Complete:{0}”,DateTime.Now);}现在还不是很糟糕只不过在每个实现中添加了几行代码而已。咱们继续往下走防御性编程因为我们的业务逻辑没有对传入的参数进行控制因此必须要检查一下是否是最坏的情景。比如如果Accrue方法传入一个null会怎样我们的业务逻辑不能处理这个所以会抛异常但我们希望它能调用我们的API处理这个异常如果处理不了就提醒UI开发者或QA发生了一些错误的东西。这种哲学就叫防御性编程只是为了减少危险场景的风险。下面我们使用防御性编程检查传入参数为null的无效场景public void Accrue(RentalAgreement agreement){//防御性编程if (agreementnull){throw new Exception(“agreement为null”);}//日志Console.WriteLine(“Accrue:{0}”,DateTime.Now);Console.WriteLine(“Customer:{0}”,agreement.Customer.Id);Console.WriteLine(“Vehicle:{0}”,agreement.Vehicle.Id);var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate);var numberOfDays (int)rentalTimeSpan.TotalDays;var pointsPerDay 1;if (agreement.Vehicle.Size Size.Luxury){pointsPerDay 2;}var points numberOfDays*pointsPerDay;//调用数据服务存储客户获得的积分_loyaltyDataService.AddPoints(agreement.Customer.Id,points);Console.WriteLine(“Accrue Complete{0}”,DateTime.Now);}我们也可以检查RentalAgreement的属性但现在上面的就足够了。Redeem的实现也有相同的问题numberOfDays参数的值不能小于1Invoice参数也不能为null因此也必须使用防御性编程public void Redeem(Invoice invoice, int numberOfDays){//防御性编程if (invoicenull){throw new Exception(“invoice为null”);}if (numberOfDays0){throw new Exception(“numberOfDays不能小于1”);}//loggingConsole.WriteLine(“Redeem:{0}”,DateTime.Now);Console.WriteLine(“Invoice:{0}”,invoice.Id);var pointsPerDay 10;if (invoice.Vehicle.SizeSize.Luxury){pointsPerDay 15;}var totalPoints pointsPerDaynumberOfDays;invoice.Discount numberOfDaysinvoice.CostPerDay;_loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);Console.WriteLine(“Redeem Complete:{0}”,DateTime.Now);}现在我们的代码开始变得具有防御性了如果在核心逻辑的控制之外发生了错误也不会影响到我们了。在添加了日志和防御性代码之后Accrue和Redeem方法开始变得有点长了也有点重复但继续看一下事务和重试逻辑。使用事务和重试如果我们使用了不止一个数据层操作为了使这些操作具有原子性那么事务是必须的。也就是说我们想要所有的数据层调用都成功提交要么都失败回滚。假设我们可以将事务放到业务逻辑层。假设底层的数据层会使用和.NET内置的事务类TransactionScope兼容的技术结合try/catch块我们可以给Accrue方法添加事务代码public void Accrue(RentalAgreement agreement){//防御性编程if (agreementnull){throw new Exception(“agreement为null”);}//日志Console.WriteLine(“Accrue:{0}”,DateTime.Now);Console.WriteLine(“Customer:{0}”,agreement.Customer.Id);Console.WriteLine(“Vehicle:{0}”,agreement.Vehicle.Id);using (var tsnew TransactionScope())//开始一个新事务{try{var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate);var numberOfDays (int)rentalTimeSpan.TotalDays;var pointsPerDay 1;if (agreement.Vehicle.Size Size.Luxury){pointsPerDay 2;}var points numberOfDays * pointsPerDay;//调用数据服务存储客户获得的积分_loyaltyDataService.AddPoints(agreement.Customer.Id, points);ts.Complete();//调用Complete方法表明事务成功提交}catch (Exception ex){throw;//没有调用Complete方法事务会回滚}}Console.WriteLine(“Accrue Complete{0}”,DateTime.Now);}记住只有调用了事务的Complete方法事务才会提交否则就会回滚。如果抛出了异常这里我们只是重新抛出相似地也可以在Redeem方法中使用TransactionScope,这里不再贴了请自行看源码。上面的代码开始变长、变丑了原始的业务逻辑代码周围包了很多和横切关注点有关的代码块logging防御性编程和事务代码。但是我们还没做完假设底层的数据持久层偶尔会出现高流量可能就会导致某些请求失败比如抛出超时异常。如果是那种情况执行几次重试会保持程序平滑运行尽管在高流量期间有点慢。通过在事务中放一个循环每次事务回滚时我们就增加重试次数一旦重试次数达到限制值我们就不管了如下public void Accrue(RentalAgreement agreement){//防御性编程if (agreementnull){throw new Exception(“agreement为null”);}//日志Console.WriteLine(“Accrue:{0}”,DateTime.Now);Console.WriteLine(“Customer:{0}”,agreement.Customer.Id);Console.WriteLine(“Vehicle:{0}”,agreement.Vehicle.Id);using (var tsnew TransactionScope())//开始一个新事务{var retries 3;//重试事务3次var succeeded false;while (!succeeded)//一直循环直到成功{try{var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate);var numberOfDays (int)rentalTimeSpan.TotalDays;var pointsPerDay 1;if (agreement.Vehicle.Size Size.Luxury){pointsPerDay 2;}var points numberOfDays * pointsPerDay;//调用数据服务存储客户获得的积分_loyaltyDataService.AddPoints(agreement.Customer.Id, points);ts.Complete();//调用Complete方法表明事务成功提交succeeded true;//成功后设置为true确保最后一次循环迭代Console.WriteLine(“Accrue Complete{0}”, DateTime.Now);//这句移入try里}catch{if (retries0){retries–;//直到尝试完次数时才重抛异常}else{throw;//没有调用Complete方法事务会回滚}} } }}相似地我们也要在Redeem方法中添加这里不做了省略。问题越来越明显了

相关新闻

深入解析Tiva EPI高级配置:多设备总线管理与时序优化实战

深入解析Tiva EPI高级配置:多设备总线管理与时序优化实战

1. 项目概述:为什么需要深入理解EPI配置?在嵌入式系统开发中,尤其是基于像TI Tiva C系列这类高性能ARM Cortex-M微控制器的项目,我们常常会遇到一个核心需求:扩展。片上资源(如SRAM、Flash)总有…

2026/7/23 6:39:34阅读更多 →
Unity高性能响应式编程:R3框架核心优势与实战应用指南

Unity高性能响应式编程:R3框架核心优势与实战应用指南

1. 项目概述:为什么Unity开发者需要关注R3?如果你在Unity项目里用过UniRx,或者对C#的LINQ和异步编程有感觉,那你肯定对“响应式编程”这个概念不陌生。简单说,就是把数据流和事件流当成一条可以观察的“河流”&#xf…

2026/7/23 6:39:34阅读更多 →
软件开发中的错误堆叠与处理最佳实践

软件开发中的错误堆叠与处理最佳实践

1. 项目概述这个标题看起来像是键盘误触或系统错误产生的随机字符串,但我们可以从另一个角度来解读——把它视为一个关于"错误处理"的隐喻性项目。在软件开发领域,错误堆叠(error stacking)是一个值得深入探讨的话题。2. 错误堆叠现象解析2.1 …

2026/7/23 6:39:34阅读更多 →
基于FPGA实现YCbCr444转RGB888

基于FPGA实现YCbCr444转RGB888

目录 一.YCbCr444转RGB888的公式算法 1.YCbCr444转RGB888计算公式 这里为什么不和RGB888转YCbCr一样放大256倍? 二.Verilog代码的实现 一.YCbCr444转RGB888的公式算法 我们通常认为YUV 和YCbCr是一个概念,但是两者还是有很大区别的: YUV是…

2026/7/23 13:43:51阅读更多 →
Sub2API:AI API网关的商业价值与技术实现

Sub2API:AI API网关的商业价值与技术实现

1. Sub2API:AI API中转服务的商业逻辑与技术实现 最近在开发者圈子里流传着一个有趣的现象:有人通过转售ChatGPT API实现了年入百万。这背后离不开一个关键工具——Sub2API。作为一款开源AI API网关平台,它让普通开发者也能成为AI服务的"…

2026/7/23 13:43:51阅读更多 →
2026上海小程序开发公司评测企业私域项目怎么选

2026上海小程序开发公司评测企业私域项目怎么选

上海企业做小程序,很多时候不是因为“小程序便宜”,而是因为它离客户近。微信里能打开,不用下载安装,适合做会员、预约、私域运营、售后服务和轻量交易。这个入口确实方便,但小程序项目一旦放进真实业务里,…

2026/7/23 13:43:51阅读更多 →
神玑NX9031X芯片解析:智能汽车软硬件升级的核心技术

神玑NX9031X芯片解析:智能汽车软硬件升级的核心技术

1. 先搞清楚这次升级到底解决了什么问题 如果你最近关注过智能汽车领域,应该会注意到“乐道全系车型完成智能软硬件大升级”这个消息。但很多人看到这种新闻的第一反应是:这到底意味着什么?是车机系统变流畅了,还是辅助驾驶更强了…

2026/7/23 13:43:51阅读更多 →
Kimi K3上线OpenRouter:AI模型部署的基础设施挑战与优化策略

Kimi K3上线OpenRouter:AI模型部署的基础设施挑战与优化策略

最近AI圈有个现象值得关注:Kimi K3上线仅两天就冲上OpenRouter平台第十大模型,但随之而来的却是基础设施不堪重负的消息。这背后反映的不仅是模型性能的突破,更是当前AI服务部署面临的真实挑战。 如果你正在考虑接入Kimi K3或其他大模型&…

2026/7/23 13:43:51阅读更多 →
算法:回溯算法

算法:回溯算法

引言 40. 组合总和 II - 力扣(LeetCode) 93. 复原 IP 地址 - 力扣(LeetCode) 78. 子集 - 力扣(LeetCode) 491. 非递减子序列 - 力扣(LeetCode) 46. 全排列 - 力扣(L…

2026/7/23 13:41:51阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/23 0:56:31阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/23 0:56:31阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 0:56:31阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:00:28阅读更多 →
从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:28阅读更多 →
油泥处理设备哪里能买到

油泥处理设备哪里能买到

油泥处理设备哪里有?这是许多从事油田、炼化、清罐业务的从业者最关心的问题。根据河南三丰环保设备有限公司的行业经验,选购油泥处理设备的核心在于设备能否适配当地环保法规与原料特性,而非单纯看价格。该公司总经理王钦田先生指出&#xf…

2026/7/23 0:00:28阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/22 22:56:18阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/22 18:55:50阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/22 18:55:50阅读更多 →