C# ATM模拟器:分层架构与状态机设计实战
1. 项目概述一个看似简单却暗藏工程逻辑的ATM模拟器“C#——银行ATM程序DEMO”这个标题乍看像是大学C#课程期末作业的常见命题甚至可能被归类为“过时的控制台小练习”。但在我带过的多个实训项目中恰恰是这类表面朴素的题目最能暴露开发者对分层设计意识、状态管理能力、异常边界把控这三项核心工程素养的真实水平。它不是在考你会不会写Console.WriteLine()而是在考你能否把“取款”“查询余额”“修改密码”这些用户动作稳稳地锚定在业务规则约束、数据一致性保障、人机交互容错性三重坐标系里。我见过太多人用一个switch语句堆出全部功能运行起来能动但只要把“输入非数字”“连续输错三次密码”“账户余额不足却强行取款”这几个场景抛过去整个流程就立刻崩成一地碎片。这个DEMO真正的价值从来不在界面多炫而在于它是一面镜子——照出你写代码时脑子里有没有装着真实银行系统的影子资金不能凭空产生或消失操作必须可追溯失败必须有明确归因用户永远会做你没预料到的事。它适合两类人深度实践一是刚学完C#基础语法、正卡在“如何组织代码”瓶颈期的初学者二是已有开发经验、想系统梳理“面向对象建模异常驱动设计”落地路径的进阶者。如果你的目标是做出一个能经得起5分钟连续压力测试比如疯狂按错键、反复插卡拔卡的稳定原型那接下来的每一步都值得你慢下来亲手敲一遍。2. 整体架构设计与核心思路拆解2.1 为什么拒绝“上帝类”坚持三层分离很多初学者一上手就创建一个ATMProgram类所有方法全塞进去ShowMainMenu()、Withdraw()、CheckBalance()、ChangePin()……逻辑看似清晰实则埋下三颗雷第一业务逻辑与界面强耦合——一旦要把控制台换成WinForm90%代码得重写第二数据持久化无法替换——当前用内存List存用户明天要连SQL Server就得翻遍所有方法改数据库访问代码第三单元测试几乎不可能——Withdraw()方法里混着Console.ReadLine()和Console.WriteLine()你怎么用测试框架去断言它的行为我坚持采用经典的三层架构表示层Presentation Layer负责纯界面交互只管“怎么问、怎么答”业务逻辑层Business Logic Layer是核心大脑封装所有规则判断如“取款金额必须是100倍数”“单日取款上限5000元”数据访问层Data Access Layer则像一个哑巴仓库只提供GetUserById()、UpdateUser()这类原子操作。这三层之间通过接口Interface通信比如业务层不依赖具体的数据类而是依赖IUserRepository接口。这样做的好处是当某天需求变成“支持指纹识别登录”你只需新增一个FingerprintAuthenticator类实现IAuthenticator接口表示层调用方式完全不变——这才是可维护性的起点。2.2 状态机思维ATM不是静态菜单而是动态流程ATM的本质是一个有限状态机FSM。它不是永远停留在“主菜单”状态而是在“插卡待验证”→“密码验证中”→“验证成功/失败”→“主功能选择”→“取款中/查询中/改密中”→“交易完成/异常退出”等状态间流转。如果用if-else硬编码所有跳转代码会迅速变成意大利面条。我的方案是定义一个ATMState枚举public enum ATMState { Idle, // 空闲等待插卡 CardInserted, // 卡已插入等待输入PIN PinVerified, // PIN验证通过显示主菜单 Withdrawing, // 正在取款流程 CheckingBalance,// 正在查询余额 ChangingPin, // 正在修改密码 TransactionComplete, // 交易完成等待退卡 ErrorState // 错误状态如PIN错误3次锁定 }然后在主循环中用一个currentState变量驱动流程while (currentState ! ATMState.Idle) { switch (currentState) { case ATMState.CardInserted: currentState HandleCardInsert(); break; case ATMState.PinVerified: currentState ShowMainMenu(); break; // ... 其他状态处理 } }这种设计让逻辑异常清晰每个状态只关心“自己该做什么”和“下一步去哪”避免了全局变量满天飞和状态判断散落在各处的混乱。更重要的是它天然支持“中断恢复”——比如用户在取款中途按取消键状态直接切回PinVerified而不是靠一堆goto或标志位去“救火”。2.3 安全边界从“能运行”到“防误操作”的思维跃迁银行系统最忌讳“默认信任”。这个DEMO必须预设所有用户都是“不可信的”输入校验不是锦上添花而是第一道防火墙。Console.ReadLine()读到的永远是字符串你绝不能直接int.Parse()——用户输入abc或100.5会直接抛出FormatException。正确做法是int.TryParse(input, out int amount)失败时返回友好提示“请输入有效的整数金额”。业务规则必须硬编码进逻辑层。比如取款金额必须满足① 是100的整数倍ATM机物理限制② 不超过账户余额③ 不超过单日累计取款限额假设5000元。这三条缺一不可且必须在Withdraw()方法内部完成校验而不是指望前端“别让用户输错”。敏感操作必须二次确认。修改密码前必须要求用户先输入旧密码验证身份再输入两次新密码防输错。这里有个细节新密码两次输入不一致时不能直接退出而应清空当前输入让用户重新输入旧密码——因为旧密码验证通过后用户才获得“修改权限”这个权限状态需要被显式管理。提示我曾在一个真实项目中看到因忘记校验“取款金额是否为100倍数”导致用户输入250元时系统扣款250但吐钞200机器只支持100元面额引发客诉。技术上只是加一行if (amount % 100 ! 0)但背后是对物理设备约束的理解。3. 核心模块详解与关键实现细节3.1 数据模型设计用领域语言定义实体很多人一上来就想着“怎么存数据”却忽略了“数据代表什么”。在银行领域“用户”不是简单的ID姓名密码而是持卡人CardHolder和银行卡BankCard两个概念。一张卡对应一个持卡人但一个持卡人可能有多张卡虽然DEMO只模拟一张。因此我定义了两个核心类// 持卡人实体 - 代表真实的人 public class CardHolder { public string Id { get; set; } // 唯一标识如身份证号哈希 public string Name { get; set; } // 姓名 public DateTime CreatedAt { get; set; }// 开户时间 } // 银行卡实体 - 代表金融工具 public class BankCard { public string CardNumber { get; set; } // 卡号16位隐藏中间8位显示 public string PinHash { get; set; } // 密码SHA256哈希值绝不存明文 public decimal Balance { get; set; } // 账户余额 public int DailyWithdrawalLimit { get; set; } // 单日取款限额 public int WithdrawalCountToday { get; set; } // 今日已取款次数 public bool IsLocked { get; set; } // 是否因输错密码被锁定 public DateTime LastFailedLogin { get; set; } // 上次输错时间用于冷却期 public string HolderId { get; set; } // 关联持卡人ID }关键点解析密码绝不存明文PinHash字段存储的是用户输入PIN后计算的SHA256哈希值。验证时对用户输入的新PIN同样哈希再与存储值比对。这是安全底线哪怕DEMO只跑在本地也要养成习惯。余额与限额分离Balance是实时资金DailyWithdrawalLimit是风控策略二者独立变化。取款时需同时检查Balance amount和DailyWithdrawalLimit - (今日已取总额) amount。锁定机制含时间维度IsLocked是状态LastFailedLogin是时间戳。当用户第三次输错设置IsLockedtrue并记录时间下次登录时先检查DateTime.Now.Subtract(LastFailedLogin).TotalMinutes 30超时则自动解锁。这比单纯布尔值更贴近真实风控逻辑。3.2 业务逻辑层规则引擎的雏形业务逻辑层的核心是ATMBusinessService类它不碰任何Console只接收参数、执行规则、返回结果。以取款为例其方法签名和骨架如下public class ATMBusinessService { private readonly IUserRepository _userRepository; private readonly ITransactionLogger _logger; public ATMBusinessService(IUserRepository userRepository, ITransactionLogger logger) { _userRepository userRepository; _logger logger; } /// summary /// 执行取款操作 /// /summary /// param namecardNumber卡号/param /// param nameamount取款金额/param /// returns操作结果成功/失败及原因/returns public OperationResultdecimal Withdraw(string cardNumber, decimal amount) { try { // 步骤1查卡是否存在且未锁定 var card _userRepository.GetCardByNumber(cardNumber); if (card null) return OperationResultdecimal.Failure(卡号不存在); if (card.IsLocked) return OperationResultdecimal.Failure(卡片已被锁定请联系银行); // 步骤2校验金额合规性100倍数、不超余额、不超日限额 if (amount 0) return OperationResultdecimal.Failure(取款金额必须大于0); if (amount % 100 ! 0) return OperationResultdecimal.Failure(取款金额必须是100的整数倍); if (amount card.Balance) return OperationResultdecimal.Failure($余额不足当前余额{card.Balance:C}); var todayWithdrawn _userRepository.GetTodayWithdrawnAmount(cardNumber); if (todayWithdrawn amount card.DailyWithdrawalLimit) return OperationResultdecimal.Failure($超出单日取款限额今日已取{todayWithdrawn:C}限额{card.DailyWithdrawalLimit:C}); // 步骤3执行扣款关键事务性 card.Balance - amount; card.WithdrawalCountToday; _userRepository.UpdateCard(card); // 步骤4记录流水异步不影响主流程 _logger.LogTransaction(new TransactionLog { CardNumber cardNumber, Type Withdrawal, Amount amount, Timestamp DateTime.Now, NewBalance card.Balance }); return OperationResultdecimal.Success(amount, $取款成功新余额{card.Balance:C}); } catch (Exception ex) { // 记录系统级异常如数据库连接失败 _logger.LogError($取款操作异常{ex.Message}, ex); return OperationResultdecimal.Failure(系统繁忙请稍后再试); } } }这里的关键设计OperationResult泛型封装统一返回结构包含IsSuccess、Data成功时的返回值、Message提示信息。避免用null或throw exception来表示业务失败因为后者会打断正常流程。事务性保障UpdateCard()必须是原子操作。在DEMO中可用lock或SemaphoreSlim保证内存数据更新线程安全若对接数据库则需显式开启事务。日志解耦ITransactionLogger接口允许后续轻松替换为文件日志、数据库日志或云日志服务业务代码零修改。3.3 表示层让控制台交互“有温度”控制台不是冰冷的命令行而是用户与系统的第一次握手。好的交互设计能让用户感觉“系统懂我”。以下是几个提升体验的细节智能输入提示在输入卡号时显示请输入16位卡号例1234567890123456并在用户输入不足16位时主动提示“卡号长度不足请重新输入”。密码输入掩码Console.ReadKey(true)读取每个字符但不显示用*代替避免密码明文暴露在屏幕上。菜单导航优化主菜单用数字编号但同时支持字母快捷键如“1. 查询余额”也响应“B”键降低用户记忆负担。错误恢复引导当用户输入无效选项如输入“5”不直接退出而是显示“无效选择请输入1-4之间的数字”并重新显示菜单。一个典型的菜单显示方法private void ShowMainMenu() { Console.Clear(); Console.WriteLine(.PadLeft(40, )); Console.WriteLine( 欢迎使用ATM自助服务系统); Console.WriteLine(.PadLeft(40, )); Console.WriteLine(); Console.WriteLine(1. 查询余额); Console.WriteLine(2. 取款); Console.WriteLine(3. 修改密码); Console.WriteLine(4. 退出系统); Console.WriteLine(); Console.Write(请选择操作1-4); }注意Console.Clear()在每次进入新状态前调用避免屏幕信息堆积造成混淆。这是很多DEMO忽略的细节但对用户体验影响巨大。4. 实操全流程与关键环节实现4.1 环境准备与项目结构搭建使用Visual Studio 2022社区版免费创建一个**.NET 6 Console App**项目。项目结构按分层思想组织ATMDemo.Core类库项目存放CardHolder、BankCard、OperationResultT等核心模型和接口IUserRepository、ITransactionLogger。ATMDemo.Data类库项目实现IUserRepository内存版InMemoryUserRepository和ITransactionLogger文件日志FileTransactionLogger。ATMDemo主控制台项目引用前两个项目包含Program.cs和表示层逻辑。为什么用.NET 6它是长期支持LTS版本语法简洁如顶级语句且兼容性好。避免用.NET Core 3.1已停更或.NET 7/8非LTS企业环境接受度低。初始化内存数据模拟3个测试用户// 在InMemoryUserRepository的构造函数中 _users new ListCardHolder { new CardHolder { Id HOLDER001, Name 张三 }, new CardHolder { Id HOLDER002, Name 李四 }, new CardHolder { Id HOLDER003, Name 王五 } }; _cards new ListBankCard { new BankCard { CardNumber 1234567890123456, PinHash ComputeSha256Hash(1234), // 明文密码1234的哈希 Balance 10000m, DailyWithdrawalLimit 5000m, HolderId HOLDER001 }, // ... 其他测试卡 };实操心得哈希计算工具类ComputeSha256Hash()必须放在Core层确保所有项目都能复用。我习惯把它做成static方法传入字符串返回Base64编码的哈希值便于调试时核对。4.2 主程序循环状态驱动的骨架代码Program.cs是整个DEMO的指挥中心其核心是状态机循环。完整代码骨架如下class Program { static void Main(string[] args) { // 初始化依赖 var userRepository new InMemoryUserRepository(); var logger new FileTransactionLogger(); var businessService new ATMBusinessService(userRepository, logger); var atmPresenter new ATMPresenter(businessService); // 表示层协调者 ATMState currentState ATMState.Idle; Console.WriteLine(ATM系统启动中...请插入您的银行卡); Console.WriteLine(按任意键开始); Console.ReadKey(); while (true) { switch (currentState) { case ATMState.Idle: Console.Clear(); Console.WriteLine(请插入银行卡...); Console.WriteLine(按Q退出系统); var key Console.ReadKey(true); if (key.Key ConsoleKey.Q) Environment.Exit(0); currentState ATMState.CardInserted; break; case ATMState.CardInserted: currentState atmPresenter.HandleCardInsert(); break; case ATMState.PinVerified: currentState atmPresenter.ShowMainMenu(); break; case ATMState.Withdrawing: currentState atmPresenter.HandleWithdrawal(); break; // ... 其他状态处理 default: Console.WriteLine(系统进入未知状态正在重启...); currentState ATMState.Idle; break; } } } }关键点依赖注入雏形atmPresenter持有businessService将业务逻辑与界面分离。HandleCardInsert()等方法在ATMPresenter中实现只负责Console交互和调用businessService的方法。退出机制优雅全局按Q键退出而非在每个菜单里重复写“按Q退出”。这体现了“关注点分离”——退出是系统级行为不是某个功能的附属。未知状态兜底default分支防止状态机失控强制回到Idle避免死循环。4.3 密码修改功能安全与体验的平衡术修改密码是高风险操作必须兼顾安全性和易用性。流程设计为输入原卡号 → 2. 输入原PIN验证身份→ 3. 输入新PIN → 4. 再次输入新PIN确认→ 5. 比对两次新PIN → 6. 更新密码哈希。ATMPresenter中的实现要点原PIN验证独立成步调用businessService.VerifyPin(cardNumber, inputPin)成功才进入下一步。避免把验证和修改写在一个方法里。新PIN输入两次用ReadPassword()方法封装了Console.ReadKey(true)循环分别存入newPin1和newPin2。严格比对if (newPin1 ! newPin2)提示“两次输入的新密码不一致”并清空newPin1和newPin2让用户从头输入包括原PIN。因为原PIN验证通过后用户才获得修改权限权限状态需重置。哈希更新原子性businessService.ChangePin(cardNumber, newPin1)内部必须先验证原PIN再计算新哈希最后更新数据库三步缺一不可。private string ReadPassword(string prompt) { Console.Write(prompt); var password new StringBuilder(); while (true) { var key Console.ReadKey(true); if (key.Key ConsoleKey.Enter) break; if (key.Key ConsoleKey.Backspace password.Length 0) { password.Remove(password.Length - 1, 1); Console.Write(\b \b); // 退格并覆盖星号 } else if (char.IsLetterOrDigit(key.KeyChar)) { password.Append(key.KeyChar); Console.Write(*); } } Console.WriteLine(); return password.ToString(); }实操心得Backspace处理是提升体验的关键。很多DEMO忽略退格导致用户输错只能重来。这段代码实现了真正的“所见即所得”密码输入虽是小细节但专业感立现。4.4 日志与审计让每一次操作都有迹可循银行系统的核心要求是可审计性。即使DEMO不连数据库也要有完整的操作日志。FileTransactionLogger实现如下public class FileTransactionLogger : ITransactionLogger { private readonly string _logFilePath atm_transactions.log; public void LogTransaction(TransactionLog log) { try { var line ${DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}|{log.CardNumber}|{log.Type}|{log.Amount:C}|{log.NewBalance:C}|{log.Timestamp:HH:mm:ss}; File.AppendAllText(_logFilePath, line Environment.NewLine); } catch (Exception ex) { // 日志写入失败降级为控制台输出确保不丢失关键信息 Console.WriteLine($[WARN] 日志写入失败{ex.Message}); } } public void LogError(string message, Exception ex) { try { var line ${DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}|ERROR|{message}|{ex.GetType().Name}|{ex.StackTrace}; File.AppendAllText(_logFilePath, line Environment.NewLine); } catch { /* 降级失败静默处理 */ } } }日志文件格式示例2023-10-05 14:22:33.123|1234567890123456|Withdrawal|¥500.00|¥9500.00|14:22:33 2023-10-05 14:23:01.456|1234567890123456|BalanceInquiry||¥9500.00|14:23:01关键设计时间精度到毫秒避免同一秒内多笔交易日志顺序混乱。字段分隔明确用|分隔方便后续用Excel或脚本解析。降级策略日志写入失败时不抛异常中断主流程而是降级为Console.WriteLine确保业务不受影响。这是生产级日志的必备特性。5. 常见问题与排查技巧实录5.1 经典陷阱与解决方案速查表问题现象根本原因排查思路解决方案输入卡号后立即报错“卡号不存在”InMemoryUserRepository.GetCardByNumber()方法未正确实现或测试数据卡号与输入不匹配如多了空格在GetCardByNumber()开头加Console.WriteLine($Debug: searching for {cardNumber});对比输入值与内存中存储的卡号使用string.Trim()清理输入检查测试数据初始化代码确保卡号字符串完全一致取款成功但余额没变UpdateCard()方法未真正更新内存列表或更新了错误的对象如新建了一个BankCard实例而非修改原实例在UpdateCard()中添加Console.WriteLine($Updating card {card.CardNumber}, new balance: {card.Balance});确认是否执行到此确保_cards列表中找到的是原对象引用用FindIndex定位后直接赋值而非Add新对象连续输错3次密码后仍能登录IsLocked状态未持久化或LastFailedLogin时间戳未更新在VerifyPin()方法中输错时打印$Lock status: {card.IsLocked}, Last fail: {card.LastFailedLogin}每次输错都更新card.IsLocked true和card.LastFailedLogin DateTime.Now解锁检查逻辑放在验证前修改密码后用新密码无法登录新密码哈希计算错误如用了MD5而非SHA256或PinHash字段未更新到内存数据在ChangePin()方法中打印$New hash: {newHash}, stored hash: {card.PinHash}确认哈希算法一致检查UpdateCard()是否真的保存了card.PinHash的新值5.2 调试技巧让问题无处遁形日志分级在ATMBusinessService关键节点添加Console.WriteLine($[DEBUG] Withdraw step 1: card found, balance{card.Balance});。但注意这些仅用于开发调试发布前应移除或用条件编译#if DEBUG包裹。断点组合技在VerifyPin()和Withdraw()的入口处设断点观察card对象的Balance、IsLocked等属性值。特别注意card是值类型还是引用类型——BankCard是class引用类型修改其属性会影响原对象这是正确行为。输入模拟法为快速测试写一个TestInputSimulator方法预设输入序列var inputs new Queuestring(new[] { 1234567890123456, 1234, 2, 500, Y });在Console.ReadLine()处返回队列首项。这样可自动化跑通整个取款流程无需手动按键。5.3 性能与扩展性思考DEMO之外的延伸这个DEMO当前是单机内存版但它的架构已为扩展铺平道路接入数据库只需实现IUserRepository接口的SqlUserRepository类用Dapper或Entity Framework Core操作SQL Server。ATMBusinessService代码完全不用改。支持多终端表示层可拆分为ATMConsolePresenter和ATMWebPresenterASP.NET Core MVC共用同一个ATMBusinessService。增加风控规则在Withdraw()方法中加入“同一IP地址1小时内取款超3次则临时锁定”的逻辑只需新增一个IRiskControlService依赖。我个人在实际操作中发现真正拉开差距的不是谁能写出功能而是谁能在写第一行代码时就为第100次需求变更留好接口。这个ATM DEMO就是训练这种“前瞻性设计思维”的最佳沙盒——它足够小让你能看清每一粒沙又足够真让你必须直面资金、安全、体验这三座大山。当你把“取款500元”这件事拆解成状态流转、规则校验、日志记录、异常兜底的完整链条时你就已经跨过了从“写代码”到“做系统”的门槛。

相关新闻

压力测试实战:从阶梯加压到瓶颈定位的完整流程与避坑指南

压力测试实战:从阶梯加压到瓶颈定位的完整流程与避坑指南

把压力测试和性能调优做成一套能落地的流程,而不是零散地“压一压、调一调”,这件事本身比想象中的复杂。很多团队其实不缺工具,缺的是一套完整的思维框架:从目标设定、场景设计、指标观测,到瓶颈定位、调优验证、回归…

2026/10/12 20:43:23 阅读更多 →
Python爬虫框架大比拼:从requests到Selenium,TaoToken统一Key打通采集链路

Python爬虫框架大比拼:从requests到Selenium,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/12 20:43:23 阅读更多 →
策略模式在半导体可靠性测试上位机中的应用实践

策略模式在半导体可靠性测试上位机中的应用实践

我先坦白一个判断:但凡做过半导体测试系统上位机的人,看到“策略模式”四个字,多半会先愣一下——因为这活儿听起来太“业务重、学问深”了,怎么和设计模式扯上关系?但实际上,我恰恰是在一个可靠性测试机上…

2026/10/12 20:43:22 阅读更多 →

最新新闻

VUE集成UEditor完整指南:从组件封装到图片上传与问题排查

VUE集成UEditor完整指南:从组件封装到图片上传与问题排查

把VUE集成UEditor这事儿,你在搜索引擎里能翻出一百篇帖子,但真正能一次跑通的,估计不到三成。写帖子的人大多是贴了个组件就完事,没告诉你UEditor那套偏老旧的纯前端架构,跟现在VUE工程化构建之间隔了多少条沟。你照着…

2026/10/12 21:31:18 阅读更多 →
投影仪标定实战:从针孔模型到光束法平差的完整指南

投影仪标定实战:从针孔模型到光束法平差的完整指南

简介:ProCamCalib 是一款面向计算机视觉与三维重建研究者的投影仪-相机系统校准工具,用 Java 编写,可对投影机-相机系统完成几何与颜色校准,并支持多相机、多投影仪配置。它通过置于原点的首台相机进行立体校准,校准板…

2026/10/12 21:31:18 阅读更多 →
进程控制块PCB:操作系统进程控制的物理基石

进程控制块PCB:操作系统进程控制的物理基石

1. 这道题为什么让很多人栽在“进程控制块”上?“第2单元进程的描述与控制考试T1”——光看标题,你可能以为这只是某次课堂小测的普通编号题。但实际接触过的学生反馈,这道题的失分率常年高于65%,远超同卷其他基础题。它不考代码、…

2026/10/12 21:31:18 阅读更多 →
大厂Java社招面试高频题与解析:JVM、并发、MySQL、Redis全覆盖

大厂Java社招面试高频题与解析:JVM、并发、MySQL、Redis全覆盖

面完一轮头部互联网公司的社招Java岗,我把整轮面试遇到的高频题和参考答案重新整理了一遍。一开始是打算凭记忆复盘的,结果越整理越觉得,大厂社招Java面试其实翻来覆去就那么几座山:JVM、并发、MySQL、Redis、分布式场景、还有一两…

2026/10/12 21:31:17 阅读更多 →
OpenClaw深度解析:从零构建你的个人AI助手(金元宝生成)

OpenClaw深度解析:从零构建你的个人AI助手(金元宝生成)

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

2026/10/12 21:31:17 阅读更多 →
STM32定时器编码器四倍频原理与配置实战

STM32定时器编码器四倍频原理与配置实战

1. 为什么编码器四倍频值得单独拿出来说做电机控制或者位置检测的朋友,对增量式编码器肯定不陌生。这种编码器一般输出A、B两路正交信号,转一圈输出固定数量的脉冲。但很多人第一次用STM32的定时器接编码器时,会发现读出来的计数值和手册上的…

2026/10/12 21:30:16 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →