Autofac集成Castle DynamicProxy实现.NET Core AOP日志拦截实战
如果你手头有一套 .NET Core 服务里面几十个业务方法都需要记录“开始调用、结束调用、入参出参、耗时、异常”最朴素的做法是每个方法里面手动敲几行日志代码。敲到一半你就会意识到日志这种横切逻辑和业务逻辑强耦在一起既污染代码又容易漏记。更好的做法是用 AOP面向切面编程将拦截逻辑集中到一处在方法执行前后自动插入“记录”动作。这篇文章分享我在 .NET Core 里用 Autofac 集成 Castle DynamicProxy 实现 AOP 拦截给所有注册到容器的服务统一记录执行日志。我会从最基础的工程搭建讲到异步方法拦截、敏感参数脱敏、性能控制再把实战中容易踩的坑列出来适合那些已经会用 Autofac 但还没系统做过 AOP 的开发者参考。1. 为什么用 Autofac Castle 做 AOP而不是把日志写进每个方法1.1 AOP 到底帮我们省了什么AOP 的核心思路是把“非业务”的关注点从业务方法里抽出去。比如日志记录、异常监控、执行耗时、权限校验、事务处理这些都属于横切关注点。它们散落在每个方法里时你根本管不住有人记了入参没记出参有人写日志写一半抛异常有人忘记处理异步异常最终日志格式五花八门排查问题全靠肉眼猜。用 AOP 之后所有这些逻辑集中到一个拦截器里。对于业务方法来说它们感受不到拦截器的存在还是老老实实写自己的参数校验、调仓储、返回结果。拦截器在代理层做文章你通过容器拿到的不是原始实例而是一个动态代理对象代理对象会把方法调用转发给真实方法并且在转发前后插入你的统一逻辑。一个很贴切的类比业务方法是房子里干活的工人拦截器是门口的访客登记台。每个进出房间的人都要过登记台但工人自己不用管登记登记动作被隔离在门口气闸。这就是 AOP 的价值你要记录的所有方法只需要在容器注册阶段声明一次后面新增的 Service 只要走同一套注册规则拦截自动生效。1.2 在 .NET Core 里为什么优先选这套组合.NET 生态里做 AOP 的路径并不少。Controller 层有 ActionFilter底层管道有 Middleware还有一种做法是定义装饰器类手动包裹目标方法。但它们都有各自边界ActionFilter 只在 ASP.NET Core MVC 的 Action 调用链路里生效它管不到普通业务服务也不适用于非 Web 场景。装饰器模式可以控制任意对象但每包一个服务就要手写一个包装类注册时还要逐个嵌套服务一多维护成本就上来了。如果你已经用 Autofac 做依赖注入那么 Autofac.Extras.DynamicProxy 这个集成库几乎是最无痛的选择。只要在注册时加一句EnableInterfaceInterceptors()和InterceptedBy()一个可拦截的代理实例就自动生成不需要手写代理代码。底层代理引擎用的是 Castle.Core 的 DynamicProxy它在 .NET 上已经打磨了很多年是很多框架事实上的代理基础库。它支持两种拦截方式基于接口的代理以及基于类继承的子类代理。具体到日志场景接口代理更稳因为代理生成的类型实现的是接口不依赖方法是否 virtual类代理则要求方法必须可 override约束多一些。这个选型在我后续实际项目里几乎没有遇到瓶颈稳定性可以放心。2. 起步最小工程、依赖包和两条代理约定2.1 先搭一个接口加实现的最小工程我在演示项目里写的是一套极简业务服务接口和实现分离所有方法都是异步风格因为实际业务里绝大多数服务方法都是 async。假设有一个IUserService里面包含了新增用户、根据 Id 查询用户这两个方法。public interface IUserService { Tasklong CreateAsync(CreateUserRequest request); TaskUserInfo GetByIdAsync(long id); } public class UserService : IUserService { public async Tasklong CreateAsync(CreateUserRequest request) { // 模拟业务逻辑 await Task.Delay(20); return 1001; } public async TaskUserInfo GetByIdAsync(long id) { await Task.Delay(10); return new UserInfo { Id id, Name demo }; } }对应的 DTO 类就是普通的CreateUserRequest和UserInfo一个带Name、Password等属性一个带Id、Name属性。这个结构很常见真正要落地时你不需要为 AOP 额外改造这些类型。2.2 安装三个 NuGet 包接下来给项目安装 Autofac 基础设施。我通常需要这三个包dotnet add package Autofac dotnet add package Autofac.Extensions.DependencyInjection dotnet add package Autofac.Extras.DynamicProxy其中Autofac.Extensions.DependencyInjection用来把 Autofac 接入 .NET Core 的宿主系统让整个应用的服务解析走 Autofac 容器Autofac.Extras.DynamicProxy提供InterceptedBy、EnableInterfaceInterceptors这些扩展方法。这个包会传递依赖Castle.CoreProxy 引擎本身就在 Castle.Core 里所以不需要再手动装 Castle 包。如果你的项目日志系统用的是Microsoft.Extensions.Logging它已经在宿主里自带不用单独引入。版本方面我建议直接使用当前稳定版。不要特意去锁很老的版本尤其是Autofac.Extras.DynamicProxy的早期版本对某些 .NET 版本行为有差异稳定版反而省心。2.3 两条代理约定接口代理与类代理这里必须先讲清楚动态代理能生效的前提不然你照着抄代码会发现拦截器不触发。情况一接口代理。注册时用EnableInterfaceInterceptors()容器会创建一个动态代理类型这个代理类型实现你注册的接口并把调用转发给真实实现。接口上的所有公共方法都会被拦截不要求实现方法带virtual。情况二类代理。注册时用EnableClassInterceptors()容器会创建一个继承自目标类的子类并重写父类的方法。这就强制要求被拦截的方法必须是virtual否则子类无法重写调用就直接走父类方法拦截器形同虚设。在实际注册中如果一个类同时实现了接口但你不小心用AsSelf()注册并且没有启用EnableInterfaceInterceptors那拦截也一样不会生效。下面这组注册就是最典型的正确姿势var builder WebApplication.CreateBuilder(args); // 关键替换默认服务工厂 builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); builder.Host.ConfigureContainerContainerBuilder(container { container.RegisterTypeMethodLogInterceptor().AsSelf().SingleInstance(); container.RegisterTypeUserService() .AsIUserService() .InstancePerLifetimeScope() .InterceptedBy(typeof(MethodLogInterceptor)) .EnableInterfaceInterceptors(); }); var app builder.Build();我把拦截器本身也注册到了容器里这样它内部可以通过构造函数注入ILoggerT。Autofac.Extras.DynamicProxy在代理创建时会解析InterceptedBy指定的拦截器类型因此注册里必须能解析到这个类型否则启动会直接抛异常。3. 第一个拦截器把开始、结束、异常和耗时全部记下来3.1 从 IInterceptor 开始的完整实现先写拦截器本体。Castle 的IInterceptor接口只有一个方法Intercept类型是同步的但这不代表它不能处理异步方法后面我会专门解释异步的陷阱。先看一个同时覆盖同步和异步的版本public class MethodLogInterceptor : Castle.DynamicProxy.IInterceptor { private readonly ILoggerMethodLogInterceptor _logger; private static readonly JsonSerializerOptions JsonOpts new() { PropertyNamingPolicy JsonNamingPolicy.CamelCase, ReferenceHandler ReferenceHandler.IgnoreCycles, MaxDepth 8 }; public MethodLogInterceptor(ILoggerMethodLogInterceptor logger) { _logger logger; } public void Intercept(IInvocation invocation) { var methodName ${invocation.TargetType.FullName}#{invocation.Method.Name}; var stopwatch Stopwatch.StartNew(); _logger.LogInformation([AOP] 开始调用{Method}入参{Arguments}, methodName, FormatArguments(invocation.Arguments)); try { invocation.Proceed(); } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, [AOP] 调用发生同步异常{Method}耗时{Elapsed}ms, methodName, stopwatch.ElapsedMilliseconds); throw; } if (invocation.ReturnValue is Task task) { invocation.ReturnValue WrapAsyncResult(methodName, stopwatch, task); } else { stopwatch.Stop(); _logger.LogInformation([AOP] 结束调用{Method}耗时{Elapsed}ms返回{Return}, methodName, stopwatch.ElapsedMilliseconds, invocation.ReturnValue); } } private object WrapAsyncResult(string methodName, Stopwatch stopwatch, Task task) { if (task.GetType().IsGenericType) { var resultType task.GetType().GetGenericArguments()[0]; var wrapMethod typeof(MethodLogInterceptor) .GetMethod(nameof(WrapAsyncWithResult), BindingFlags.Instance | BindingFlags.NonPublic)! .MakeGenericMethod(resultType); return wrapMethod.Invoke(this, new object[] { methodName, stopwatch, task })!; } return WrapAsyncVoid(methodName, stopwatch, task); } private async Task WrapAsyncVoid(string methodName, Stopwatch stopwatch, Task task) { try { await task; stopwatch.Stop(); _logger.LogInformation([AOP] 结束调用{Method}耗时{Elapsed}ms, methodName, stopwatch.ElapsedMilliseconds); } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, [AOP] 调用发生异步异常{Method}耗时{Elapsed}ms, methodName, stopwatch.ElapsedMilliseconds); throw; } } private async TaskT WrapAsyncWithResultT(string methodName, Stopwatch stopwatch, TaskT task) { try { var result await task; stopwatch.Stop(); _logger.LogInformation([AOP] 结束调用{Method}耗时{Elapsed}ms返回{Return}, methodName, stopwatch.ElapsedMilliseconds, result); return result; } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, [AOP] 调用发生异步异常{Method}耗时{Elapsed}ms, methodName, stopwatch.ElapsedMilliseconds); throw; } } }这个版本同时考虑了普通异步方法和异步泛型返回值方法。invocation.Proceed()执行完之后如果是异步方法invocation.ReturnValue会是一个Task或TaskT我们不能在Intercept里直接等待因为接口是同步方法。正确的做法是把原来的 task 包一层新的 async 方法返回这样调用方拿到的依然是 Task但内部已经附加了我们的日志逻辑。3.2 注册到 Autofac 里的两种姿势上面我展示了单个服务的注册方式。实际上服务多起来之后逐条RegisterType写容易漏更推荐用程序集批量注册。下面这个写法是常见套路按服务命名后缀自动注册container.RegisterAssemblyTypes(typeof(UserService).Assembly) .Where(t t.Name.EndsWith(Service) t.IsPublic) .AsImplementedInterfaces() .InstancePerLifetimeScope() .InterceptedBy(typeof(MethodLogInterceptor)) .EnableInterfaceInterceptors();使用批量注册时有一点要特别注意如果某些类型没有实现接口AsImplementedInterfaces()会让他们无法被正常解析。批量注册适合你内部有统一 Service 命名约定的项目。如果项目里存在不需要日志的类比如纯粹的静态工具类也可以在Where条件里把它排除掉。还有一种情况是只对个别方法做精细控制不想全量拦截。那就不走批量注册只对目标服务单独注册或者在拦截器内部通过特性判断来跳过这个方法我放到第四章详细说。3.3 同步与异步的执行差异必须单独处理很多初学者在写第一个拦截器时会漏掉异步处理。invocation.Proceed()执行 async 方法时会立刻返回一个未完成的 Task真正的业务代码还在异步执行中。如果只在Proceed()返回后立刻记录“结束”记到的根本不是真实业务结束时间同时也捕获不到异步方法里抛出的异常。我上面代码里的三段式处理就是标准解法Proceed()同步抛出的异常由外层catch捕获Task在后续异步过程中抛出的异常由WrapAsyncVoid或WrapAsyncWithResult捕获同步方法的结果在Proceed()返回后直接记录。另外尽量不要再写async void方法。async void方法在代理层拿不到 Task异常会直接逃逸到同步上下文拦截器就管不到了这在 ASP.NET Core 里也可能引发进程级事故。我实际测过这个拦截器的效果日志输出大致长这样info: MethodLogInterceptor[0] [AOP] 开始调用Demo.Service.UserService#CreateAsync入参[arg0(typeCreateUserRequest){\name\:\demo\,\password\:\123456\}] info: MethodLogInterceptor[0] [AOP] 结束调用Demo.Service.UserService#CreateAsync耗时23ms返回1001基础能力已经通了但真实项目里问题还不少最典型的就是日志里把密码也打出来了以及每次参数都完整序列化热点接口日志过于膨胀。这些问题在下一节处理。4. 进阶日志格式、敏感信息脱敏、选择性忽略与性能控制4.1 统一日志格式并对敏感字段脱敏上面拦截器里用了FormatArguments方法这个方法我还没给出实现。实际项目里入参往往是对象不能直接ToString()我们需要序列化成 JSON但同时又需要做两件事脱敏和截断。一个低成本的实现是这样private string FormatArguments(object[] args) { if (args null || args.Length 0) { return []; } var parts args.Select((arg, index) { if (arg null) { return $arg{index}null; } var typeName arg.GetType().Name; string json; try { json JsonSerializer.Serialize(arg, JsonOpts); } catch { json $serialize_error:{typeName}; } json RedactSensitiveFields(json); if (json.Length 800) { json json[..800] ...(truncated); } return $arg{index}(type{typeName}){json}; }); return string.Join(, , parts); } private static string RedactSensitiveFields(string json) { // 覆盖常见的敏感键名统一替换成 *** return Regex.Replace( json, \(password|pwd|secret|token|apiKey|accessToken|captcha)\\\s*:\\s*\[^\]*\, \$1\:\***\, RegexOptions.IgnoreCase | RegexOptions.Compiled); }这段正则把所有形如password:123456的片段替换成password:***虽然简单但在拦截日志这种非强安全场景里够用。它也有明显盲区如果参数是数组、嵌套对象、或者敏感信息被多层包裹正则就不一定匹配得准。更严谨的方案是引入序列化时的自定义JsonConverter或者序列化后遍历 JSON 节点按属性名黑名单打码。团队有精力时可以做一套通用的脱敏组件没有精力时先守住“不让密码原文进日志”这条底线。我自己踩过的坑是某些业务对象里有循环引用直接JsonSerializer.Serialize会抛异常。所以JsonSerializerOptions里面要设置ReferenceHandler.IgnoreCycles和MaxDepth避免序列化器把整个对象图展开既防止循环也防止性能爆炸。4.2 加一个 NoLog 特性做方法级豁免有些方法确实不该记日志比如密码校验接口、大文件流下载接口或者内部高频轮询接口。与其在拦截器里写一堆方法名判断不如定义自己的特性标记在目标方法上。自定义特性很简单[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class NoLogAttribute : Attribute { }在拦截器入口加一个判断if (ShouldSkip(invocation)) { invocation.Proceed(); return; } private static bool ShouldSkip(IInvocation invocation) { var method invocation.Method; var targetMethod invocation.MethodInvocationTarget; if (targetMethod ! null targetMethod.GetCustomAttributeNoLogAttribute() ! null) { return true; } if (method ! null method.GetCustomAttributeNoLogAttribute() ! null) { return true; } var targetType targetMethod?.DeclaringType ?? method?.DeclaringType; return targetType ! null targetType.GetCustomAttributeNoLogAttribute() ! null; }为什么同时检查Method和MethodInvocationTarget因为动态代理生成的代理方法通常不会继承你标注在实现方法上的特性但MethodInvocationTarget又是原始实现方法的引用所以两个都查最稳妥。如果特性放在类级别那这个类的所有方法都被豁免。这个特性的实际价值是给团队一个显式口子不需要专门去修改拦截器代码只要在敏感或不重要的方法上标注一个[NoLog]就能跳过当前方法的日志输出调用本身仍然会正常执行。4.3 耗时统计与性能控制开关日志拦截本身也有性能成本尤其是参数序列化。如果你有个接口每秒调几千次每次都把几百 KB 的入参序列化成字符串写日志那是灾难。我在线上项目里一般给拦截器加两个开关总开关和最小耗时阈值。总开关用来在突发事故时一键关掉所有 AOP 日志。最小耗时阈值则保证大部分正常调用不记入参出参只记录慢调用、异常调用和核心链路调用。可以在拦截器构造函数里注入一个配置对象或者直接读取 IConfigurationpublic MethodLogInterceptor( ILoggerMethodLogInterceptor logger, IConfiguration? configuration null) { _logger logger; _enabled configuration?.GetValue(AopLog:Enabled, true) ?? true; _minMilliseconds configuration?.GetValue(AopLog:MinMs, 0) ?? 0; }对应Intercept开头变成var stopwatch Stopwatch.StartNew(); if (!_enabled) { invocation.Proceed(); return; } // 只做耗时统计不记录参数时的轻量分支在异步包装里记录结果前判断if (stopwatch.ElapsedMilliseconds _minMilliseconds !isError) { // 结束日志照常写但可以降为 Debug 级别或者干脆只写一条精简日志 }这样的性能控制让 AOP 不再是一个“黑盒处理器”而是可以被运维手段调节的组件。实际使用中我建议默认把完整入参日志的日志级别设成Debug把异常日志设成Error普通调用用Debug记这样生产环境默认不刷屏排查问题时再动态调整日志级别。5. 实战中的坑代理限制、生命周期和那些反直觉的行为5.1 为什么类代理必须 virtual接口代理才是长期更优解接着 2.3 节往下说。类代理的底层实现是生成目标类的子类子类重写目标类的virtual方法所以非virtual方法、静态方法、私有方法都不会被拦截。这一点很多人在自测时会踩到注册类时用了EnableClassInterceptors()某个方法忘了加virtual日志就少了。接口代理不使用继承的方式重写动态代理类型直接实现接口并把接口方法调用转发给内部持有的真实实例。从拦截粒度上说接口代理更符合“面向接口编程”的直觉你解析的是IUserService拿到的就是实现了IUserService的代理类型真实UserService反而是被包裹的对象。这也是我在生产环境默认选接口代理的原因约束少行为稳不容易因为方法修饰符而漏拦截。前提是你的服务都通过接口注册并且业务调用都通过容器解析。5.2 自调用陷阱this 调 this代理管不着这是真正让人疑惑的一个行为。假设UserService的CreateAsync内部又调用了this.GetByIdAsync即使GetByIdAsync是被拦截的方法内部这次调用的this仍然是当前真实对象不是代理对象所以不会经过拦截器。为什么很多人反复检查配置都找不到原因因为表面上看起来“我确实通过容器拿到了代理对象”可代理对象内部持有的是真实对象真实对象内部的方法调用当然走不到代理。解决办法有几个将内部需要被拦截的调用拆到另一个服务里通过构造函数注入那个服务在需要自调用的场景下把自己注入给自己即通过 Lazy 解析延迟获取代理实例或者干脆调整设计让一个 Service 方法不要过多调用兄弟方法。如果你在排查“拦截器只对部分方法生效”优先怀疑自调用。5.3 生命周期管理别把拦截器注册成与作用域冲突的实例拦截器会被代理对象调用代理对象本身又可能处于InstancePerLifetimeScope、SingleInstance等不同生命周期。这里最隐晦的坑是拦截器内部注入了作用域依赖但拦截器自身注册成了SingleInstance。举个例子如果拦截器里要读取当前请求的用户信息于是注入了一个ICurrentUser这个服务的生命周期是InstancePerLifetimeScope。拦截器却是SingleInstance那 Autofac 在解析拦截器时会捕获第一次创建它的作用域里的ICurrentUser。后续所有请求再用这个单例拦截器时拿到的还是第一次请求的用户信息这就不是日志记录问题了而是隐蔽的数据串线。一个常见的安全组合拦截器本身只依赖ILoggerT、配置对象这类根级组件注册成SingleInstance没问题。如果拦截器需要请求级数据就把拦截器注册成InstancePerLifetimeScope或者把获取请求级数据的方式改成在每次Intercept时动态解析而不是在构造函数里固定住。这类问题通常在压力测试或并发访问时才会暴露排查起来很费劲所以设计初期就要把生命周期当回事。5.4 常见症状速查表与避坑笔记下面这张表是实战里最常见的状况建议直接收藏现象常见原因解决方向拦截器完全不生效未启用EnableInterfaceInterceptors()/EnableClassInterceptors()检查注册链是否完整类代理只有部分方法被拦截被拦截的方法不是virtual给相关方法补virtual或改用接口代理日志记录顺序混乱方法内部存在自调用拆分服务或注入自身代理异步异常没有被日志捕获没有包裹 ReturnValue 中的 Task用 Task 包装方法重新赋值 ReturnValue入参是同一个对象但序列化抛异常存在循环引用设置IgnoreCycles和MaxDepth密码、Token 等敏感信息进日志序列化未脱敏加脱敏正则或自定义转换器大量日志拖慢接口每次调用全量序列化打开最低耗时阈值调整日志级别启动时提示无法解析拦截器拦截器类型未注册到容器在容器中注册拦截器测试里用new调用服务拦截不生效代理只在容器解析时生成通过容器解析对象后再测试还有几个细节是经验之谈。第一不要在拦截器里擅自修改invocation.Arguments数组代理后续会把修改后的参数传给真实方法一旦改错就是业务数据被污染。第二如果要做耗时统计Stopwatch最好在Intercept开头创建不要放到包装 Task 里再创建不然统计不到等待调度的排队时间。第三日志模板里尽量用结构化字段不要直接拼字符串这样进入日志系统后可以按照 Method 名、耗时做聚合查询。第四如果工作单位里已经有统一流程 ID 或 TraceId在拦截器入口用ILogger.BeginScope给整次调用挂一条关联链后续排查问题时能把一次请求跨越的服务串联起来。这个方案我陆陆续续在不同服务里复制过很多次每次改动的只是注册规则和脱敏清单。AOP 拦截器的核心代码长期稳定因为它解决的是“一段横切逻辑如何与业务解耦”这个通用问题。掌握这套思路之后你还可以把日志拦截器扩展成性能监控拦截器、切入事务拦截器原理一模一样只是Intercept里做的事不同。最后留一个建议不要试图拦截所有类所有方法挑好边界比如服务层门面类收益最高副作用最小。

相关新闻

科密高拍仪SDK与驱动开发指南:从安装到二次集成

科密高拍仪SDK与驱动开发指南:从安装到二次集成

简介:科密高拍仪SDK及驱动是一套面向办公自动化与文档数字化场景的软硬件集成开发包,适合需要在自研系统中嵌入高拍仪拍摄、图像处理与文件上传能力的开发者。资源包共236个文件,约691.45MB,包含50个dll动态库、22个exe可执行程序…

2026/10/16 20:53:15 阅读更多 →
Nscripter 2.96 入门:脚本语言、流程控制与视觉小说开发实战

Nscripter 2.96 入门:脚本语言、流程控制与视觉小说开发实战

简介:面向视觉小说与互动叙事游戏开发者的 Nscripter 2.96 完整资源包,内置简繁中文支持,适合无编程基础的创作者快速上手编写分支剧情与事件逻辑。压缩包共15个文件,以可执行程序、动态库及文本说明为主,总大小仅782K…

2026/10/17 21:12:26 阅读更多 →
EO.WebBrowser:.NET WinForms项目替换IE内核的Chromium浏览器控件

EO.WebBrowser:.NET WinForms项目替换IE内核的Chromium浏览器控件

简介:EO.WebBrowser v23.4.26 清洁版是一套面向 .NET 平台的浏览器引擎组件。它以 Chromium 为基础,主要帮助 C# 开发者在 WinForm 或 WPF 桌面程序中嵌入完整网页浏览能力,也支持无界面运行模式,适合网页自动化操作和数据抓取场景…

2026/10/16 20:53:15 阅读更多 →

最新新闻

VLSM可变长子网掩码实战:从FLSM翻车到CIDR路由汇总

VLSM可变长子网掩码实战:从FLSM翻车到CIDR路由汇总

1. 从一个真实翻车现场说起:为什么固定子网掩码不够用刚入行那会儿,我接手过一个园区网的地址规划。当时拿到手的任务很简单:把 192.168.10.0/24 这个 C 类网段分给四个部门用。我心想这有什么难的,直接一分为四,每个部…

2026/10/17 23:08:02 阅读更多 →
CM与CDH元数据库迁移实战:从备份到验证的完整指南

CM与CDH元数据库迁移实战:从备份到验证的完整指南

1. 元数据库迁移这件事,到底在折腾什么但凡在生产环境跑过一段时间大数据平台的人,大概率都碰过这样一个场景:集群要整体搬迁、要换机房、要做资源整合,或者干脆就是原来的元数据库服务器硬件老化撑不住了,需要把整套服…

2026/10/17 23:08:02 阅读更多 →
UE4悬停与点击效果的底层实现原理与多端适配

UE4悬停与点击效果的底层实现原理与多端适配

1. 为什么UE4里一个“鼠标悬停”要专门写一篇长文?在某跨平台系统开发中,我接手过一个被反复退回的UI模块:按钮在PC端测试时一切正常,但一接入手柄模式或切换到触摸屏设备,悬停态就彻底消失;更奇怪的是&…

2026/10/17 23:08:02 阅读更多 →
61,000+ Skills 之后:用 TaoToken 统一 Key 管好 AI Agent 的“App Store”

61,000+ Skills 之后:用 TaoToken 统一 Key 管好 AI Agent 的“App Store”

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

2026/10/17 23:08:02 阅读更多 →
VS Code 修改选中文字或代码的颜色:TaoToken 主题配色与高亮配置指南

VS Code 修改选中文字或代码的颜色:TaoToken 主题配色与高亮配置指南

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

2026/10/17 23:08:02 阅读更多 →
用Python SimPy实现离散事件仿真:从资源排队到系统优化

用Python SimPy实现离散事件仿真:从资源排队到系统优化

简介:一份面向Python开发者与系统仿真初学者的完整技术文档,以SimPy离散事件建模为主线,帮助读者从零掌握复杂系统仿真的核心方法。文档内容系统完整,先从离散事件系统的基本概念讲起,再深入讲解SimPy的环境、进程、事…

2026/10/17 23:07:01 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →