单机商城用户系统如何设计:从SQLite表结构到密码安全实战

发布时间:2026/10/21 5:54:14
单机商城用户系统如何设计:从SQLite表结构到密码安全实战
我最近翻到一个单机购物商城的源码读完用户系统这一块确实有点东西。它完全本地运行不联网所有数据都落在SQLite里但用户系统的设计一点也没糊弄注册、登录、会话管理、权限控制该有的都有。我扒完这套实现之后最大的感受是单机应用做用户系统跟平时写前后端分离的Web项目思路完全不一样很多在服务器场景下被框架隐藏掉的细节在这里都得自己动手反而更能看清用户系统设计的本质。今天这篇文章就把这套用户系统的实现拆开来讲包括数据库怎么设计、密码怎么处理、会话怎么管理、跟购物车和订单怎么关联以及我实操过程中踩过的坑。不管你是拿这个项目练手还是想自己写一个类似的本地应用这套思路都能直接抄作业。1. 单机商城为什么选SQLite用户系统的数据底座长什么样这个项目最有意思的选型决策就是用SQLite作为唯一的数据存储。说实话一个单机应用想存数据大多数人第一反应是直接读写JSON或者CSV文件简单粗暴。但这个项目没有这么做而是老老实实建了数据库表这背后的逻辑值得先说清楚。1.1 本地文件存储的三个致命问题如果你尝试过用JSON文件存用户数据很快就会碰到几个绕不开的麻烦事。首先是并发写的安全性。单机应用虽然看起来只有一个用户在用但实际上程序自己可能同时在多个线程里处理不同的事情比如后台线程在刷新商品库存、主线程在提交订单、另一个线程在写日志。如果都用JSON文件很可能出现两个线程同时往同一个文件里写数据轻则数据覆盖丢失重则文件直接损坏。SQLite天生带锁机制明确告诉你同一时间只能有一个写者剩下的排队等这就把并发安全的问题从源头解决了。其次是数据一致性的保证。购物商城最怕的就是数据写到一半出问题比如扣库存扣了但订单没生成用户的钱白白没了。如果用文件存储你得自己写一套“先备份再写入、写失败再回滚”的逻辑极其容易漏。SQLite原生支持事务要么全部成功要么全部回滚这个承诺在涉及钱和库存的场景里太重要了。第三是查询和统计的效率。用户表、订单表、商品表之间有关联关系比如要查“某个用户的所有订单”用JSON文件就得把整个文件读进内存然后遍历过滤。数据量一大这个操作又慢又费内存。SQLite用一条SQL语句就能搞定还能建索引加速性能完全不在一个量级。1.2 SQLite在本项目中的角色定位在这个购物商城项目里SQLite承担了所有数据存储职责包括用户账号信息、登录令牌、商品目录、购物车记录、订单记录、订单明细总共六张核心表。数据库文件就是一个普通的本地文件比如叫shop.db程序启动时自动连接不存在了就自动创建并初始化表结构。这个设计思路其实可以类比成一个简化版的“迷你数据库服务器”只是这个服务器不是一个独立进程而是直接以库的形式嵌入在程序里。你用到的SQL语法、事务机制、索引能力跟大型数据库几乎一致只是不需要安装、不需要配置账号密码、更不需要网络连接。把数据库嵌入到程序内部不需要单独安装数据库服务这就让整个项目的交付变得极其简单用户拿过去解压就能跑连数据库都不用装。1.3 用户系统在整套架构中的位置我现在打开项目源码里用户系统那一层把所有相关文件列出来核心模块大致分成这么几个数据库初始化模块、用户数据访问模块、密码处理工具、会话管理模块、购物车关联模块。整个用户系统是放在一个独立的包结构里的不让别的地方直接操作数据库表必须通过用户数据访问模块来读写这个分层思路在单机项目里很容易被忽略但实际价值很大后面具体看代码的时候会提到。2. 用户表结构设计每个字段都是有用意的先说核心的users表这是整套用户系统的地基。我直接贴建表SQL这是项目里最关键的一段CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, email TEXT, role TEXT NOT NULL DEFAULT customer, status INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), last_login_at TEXT );2.1 主键选择逻辑用户表的主键用的是自增整型id而不是用户名。有人可能会问用户名已经设了UNIQUE为什么不直接拿它当主键其实这是个很经典的数据库设计问题。用户名虽然唯一但它是业务字段可能以后会允许用户修改比如用户想换个昵称如果拿它当主键所有关联表的外键都得跟着改那改动成本就太大了。而自增id是纯技术字段一旦生成永不改变其他表只需要跟这个id关联就不用担心用户名变更引发连锁反应。这种代主键的设计就是典型的代理主键思维值得借鉴。2.2 密码存储字段的拆分password_hash和salt两个字段分开存这个设计很关键。密码绝不能存明文但也绝不能只存一个简单的哈希值。正确做法是“加盐哈希”在用户注册时生成一串随机数作为盐把盐加在密码后面再算哈希这样就算两个用户密码相同存储结果也完全不同可以防止彩虹表攻击。关于密码处理的下文专门有一小节详细展开。2.3 角色字段与状态字段role字段区分用户身份默认是customer普通用户管理员账号会被初始化为admin。这个字段在商城场景里非常重要因为后台管理功能比如商品上架、订单发货都必须只有管理员才能操作用户端永远碰不到管理入口。单机项目虽然没有独立的服务器做权限校验但程序内部在调用管理接口前必须检查当前登录用户的role只有admin才能放行。status字段表示账号状态1是正常0是禁用。这个字段的存在解决了一个很现实的问题用户违规了怎么办被封号总得有地方能记录如果直接删用户记录他关联的订单历史就全没了管理员想查记录都没地方查。所以用一个状态位把账号逻辑删除保留所有历史数据这是最稳妥的做法。2.4 关联表会话与购物车再看另外两张关键表。sessions表用来存“谁已经登录了”结构是这样的CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), expires_at TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) );token是一个随机生成的字符串用户登录成功后程序生成一个新token把用户id和过期时间绑定在一起。之后用户每次访问需要身份的操作就带上这个token程序拿token查sessions表能查到且没过期就算登录有效。这就是单机版的“会话”跟Web应用的Session机制思路一致只是实现载体从内存或Redis变成了SQLite表。购物车表也要关联用户CREATE TABLE IF NOT EXISTS cart_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, added_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), UNIQUE(user_id, product_id), FOREIGN KEY (user_id) REFERENCES users(id) );注意这里有个UNIQUE(user_id, product_id)约束意思是同一件商品在同一个用户的购物车里只能有一条记录如果用户再次加购同一商品走的是“更新数量”而不是“再插一条”。这个设计避免了购物车里出现两行一模一样的商品是很细节但对用户体感影响很大的点。2.5 表结构设计的经验总结看完这张表结构你会发现每个字段都不是白加的。id负责稳定关联username负责唯一标识password_hash和salt一起保障密码安全role和status负责权限与状态管理created_at和last_login_at负责追溯行为。这就是一个“信息完整、可追踪”的用户模型。我见过很多练手项目把用户表简化得只剩用户名和密码后面做到订单模块时发现没法关联购物车和订单再回来改表结构来回返工。倒不如一开始就在这个项目上老老实实按照完整业务设计表先把地基打扎实。3. 从注册到登录用户系统的核心流程拆解光有表结构还不够真正的核心在流程控制逻辑里。这个项目的用户注册和登录都是本地函数调用但流程设计非常完整我按模块来拆。3.1 注册模块的完整时序注册流程的函数逻辑简化后大致是这样的。注意这里的每一步都有前置校验不是简单往数据库里插一条记录。def register(username, password, emailNone): # 1. 合法性校验用户名长度、字符白名单、密码强度 validate_username(username) validate_password(password) # 2. 唯一性校验用户名和邮箱都不能重复 if user_exists_by_username(username): raise UserExistsError(用户名已存在) if email and user_exists_by_email(email): raise UserExistsError(邮箱已被注册) # 3. 密码加盐哈希 salt get_random_salt(16) password_hash hash_password(password, salt) # 4. 写入用户表 user_id insert_user(username, password_hash, salt, email) # 5. 创建初始购物车记录延迟创建 return user_id第一步的合法性校验容易被人偷懒跳过但实际上非常必要。用户名如果允许包含空格、特殊符号甚至SQL关键字后面查数据时容易出幺蛾子。这个项目的校验规则是用户名只能包含字母、数字、下划线长度3到20个字符密码至少要8位且同时包含字母和数字。这些都是很常规的规则但能挡住大多数低级问题。第二步唯一性校验表面上是查重实际上是给数据库的UNIQUE约束上一道双保险。就算程序这边漏查了数据库本身也会拒绝重复插入。这种“应用层校验数据库约束”的双层防护思路是数据完整性设计的经典做法。第三步的密码处理是整个注册流程的核心下一节单独展开讲。第四步插库本身很简单。但这里有一个容易被忽略的点用户名带有唯一的约束冲突时程序要先判断是并发重复提交导致的还是用户自己重复点击导致的再返回给用户对应的提示信息这个属于用户体验层面。第五步的购物车初始化其实不需要在注册时立即执行因为购物车记录可以在用户首次加购时才通过user_id动态创建这属于“懒加载”的思路。当然也有项目选择注册时直接插一条空的购物车记录两种方案在数据量小的时候没什么差别但项目目前采用的是懒加载。3.2 登录模块的认证与防暴力破解登录函数的核心逻辑def login(username, password): user find_user_by_username(username) if not user: raise InvalidCredentialsError(用户名或密码错误) # 账号状态检查 if user.status ! 1: raise AccountDisabledError(账号已被禁用) # 密码校验用哈希结果比对 input_hash hash_password(password, user.salt) if not constant_time_compare(input_hash, user.password_hash): raise InvalidCredentialsError(用户名或密码错误) # 登录成功更新最后登录时间 update_last_login(user.id) # 生成会话令牌 token generate_token() save_session(token, user.id, expires_in_hours72) return token, user.basic_info()整个登录过程有两个细节值得单独讲。第一是错误提示的设计当用户名不存在和密码错误时返回的是同一个提示“用户名或密码错误”。这么做是故意的目的是防止攻击者通过不同的错误提示来猜测哪些用户名已经注册过。在单机应用里这个风险可能不那么高但这个意识是对的安全设计就是一种“默认不信任”的思维习惯。第二是账号状态检查放在密码校验之前。这个顺序也经过考虑如果先查密码能知道密码对不对如果先查状态能知道号有没有被禁。理论上攻击者可以通过报错时机来区分状态但这个问题影响不大因为状态信息本来就不算是高敏感信息。真正要注意的是最后一步的会话令牌生成这个token必须足够随机项目里使用的是secrets模块生成的32字节随机数而不是用简单的自增id或者时间戳加用户名拼接。原因很简单如果token可预测别人就能伪造出管理员的登录状态。3.3 登录态的判断与退出机制有了token之后判断当前是否处于登录态就简单了就是一个查表操作def get_current_user(token): if not token: return None session find_session_by_token(token) if not session: return None if session.expires_at now(): delete_session(token) return None return find_user_by_id(session.user_id)这里的session有效期设为72小时也就是3天内免登录。到期之后程序会自动删除这条过期记录用户需要重新登录。项目里也做了“记住我”选项勾选之后有效期延长到30天。通过一个简单的三元判断复用同一套逻辑就行。退出登录更简单把token对应的session记录从表里删掉即可。这样设计的好处是用户手动退出后即便token被人偷走也无法使用了因为服务端或者说本地数据库里已经没有这条会话记录了。4. 密码存储的安全策略这里面的门道最多用户系统里最值得展开讲的就是密码处理。这块单机应用虽然只有本地一个人用看起来泄露风险低但密码安全永远是底线问题因为这个项目的代码万一被别人拿去部署或者用户直接把密码文件分享出去那问题就大了。安全性必须从一开始就是设计的一部分。4.1 绝对不能做明文存储先列一个绝对不能踩的红线明文密码直接存数据库。任何阶段都不该出现这种代码。如果用户的密码是以明文形式落在SQLite文件里意味着拿到数据库文件就拿到了所有账号的密码这种泄露是致命的而且毫无技术含量可言。4.2 哈希算法的选择逻辑项目里用的哈希算法是PBKDF2-HMAC-SHA256这是目前最主流的密码哈希方案之一而且在Python标准库里直接就能用不需要装额外的依赖非常适合单机项目。先解释一下什么是哈希。哈希可以理解成一个单向的“榨汁机”你把一个完整的水果丢进去出来一杯果汁但你没法从果汁里把原来的水果还原出来。密码经过哈希之后得到一串固定长度的十六进制字符串攻击者拿到这串字符串理论上没法反推出原始密码。但简单哈希有个漏洞它就是彩虹表。攻击者可以提前算好大量常见密码的哈希值建一张对照表拿到哈希串直接查表就还原出明文密码了。加盐就是为了对抗彩虹表。盐是一段随机生成的额外字符串在计算哈希之前先拼在密码后面这样同一个密码配上不同的盐产生的哈希值完全不同。攻击者就算有彩虹表也得为每个盐单独造一张表成本指数级上升实际等于废掉了彩虹表攻击。项目中使用16字节的随机盐存储格式是十六进制字符串算出的密码哈希也存成十六进制字符串。校验密码的时候从数据库取出该用户自己的盐用同样的方式拼接原始密码再算一遍哈希然后跟库里存的哈希做比对。4.3 为什么用PBKDF2而不是直接用SHA256你可能会有疑问直接用hashlib.sha256(password salt)行不行技术上完全可行但安全性不够。原因是SHA256这类通用哈希函数计算速度太快了现代电脑一秒钟能算几百万次攻击者拿到哈希值之后可以进行海量的离线猜测每秒尝试几百万个密码普通密码根本扛不住。PBKDF2的核心思路是“故意变慢”。它会在内部对密码进行成千上万次迭代计算比如这个项目设了迭代次数是10万次意味着攻击者每秒只能尝试几十到几百个密码暴力破解的时间成本一下子放大了几百上千倍。这种“刻意变慢”的算法还有一个专门的类别叫密钥派生函数跟通用哈希函数的设计目标完全不同。用生活例子来类比SHA256就像是一扇普通木门轻轻一推就开PBKDF2就像是一扇装了多重防盗锁的银行金库门每一秒只能尝试一个密码组合暴力破解变得不划算攻击者自然会放弃。Python的实现代码大概是这样的import hashlib, secrets def get_random_salt(length16): return secrets.token_hex(length) def hash_password(password, salt): digest hashlib.pbkdf2_hmac( sha256, password.encode(utf-8), salt.encode(utf-8), iterations100000 ) return digest.hex() def verify_password(password, salt, expected_hash): actual_hash hash_password(password, salt) return constant_time_compare(actual_hash, expected_hash)4.4 恒定时间比较细节里的魔鬼verify_password的最后一步我特意用了constant_time_compare而不是直接写比较。这里藏着一个安全细节常规的字符串比较一旦发现第一个字符不同就立刻返回False理论上可以通过测量响应时间逐步猜出哈希值的每一位字符。这种攻击叫时序攻击虽然实操门槛很高但既然标准库里有现成的安全比较函数为什么不直接用呢这就是所谓的“安全默认值”不增加任何成本但能挡住一个潜在隐患。4.5 一个常见的误区加密和哈希不是一回事很多人会把密码哈希和加密混为一谈其实两者原理完全不同。加密是可逆的你有密钥就能把密文还原成明文但哈希不可逆压根不存在“解密”这一步。用户系统里如果出现了“密码加密后存储、并且保存了密钥”这种设计反而更危险因为一旦密钥泄露所有密码全部还原。哈希加盐的设计目标是就算整个数据库文件被拷走攻击者也无法还原出原始密码。存储密码永远用哈希这是一种“即使数据全部泄露也不怕”的思路。5. 与购物车、订单模块的联动设计用户系统本身做得再好如果跟商城核心业务对不上也只是个空壳。这个项目里用户模块跟购物车、订单的联动同样可圈可点我来拆一下这些关联逻辑。5.1 一切以user_id为主线购物车表、订单表都以user_id作为外键凡是涉及用户数据的查询操作统一在SQL的WHERE子句里带上user_id。比如查某个用户购物车里的全部商品SELECT ci.product_id, ci.quantity, p.name, p.price FROM cart_items ci JOIN products p ON ci.product_id p.id WHERE ci.user_id ? ORDER BY ci.added_at DESC;这条SQL可能在很多项目里都是固定写法但在这个单机项目里它有一个额外的好处由于所有数据都在本地SQLite执行JOIN的速度非常快毫秒级返回用户体验跟Web应用没有任何差别。5.2 下单逻辑的事务处理订单模块是用户系统的终极考验因为涉及到多张表的联动修改稍有不慎就会让数据不一致。项目里下单的核心逻辑是这样的def create_order(user_id, items): try: db.execute(BEGIN) # 1. 计算订单总价 total 0 for item in items: product get_product(item.product_id) if product.stock item.quantity: raise InsufficientStockError(product.id) total product.price * item.quantity # 2. 插入订单主表 order_id insert_order(user_id, total) # 3. 插入订单明细 for item in items: insert_order_item(order_id, item.product_id, item.quantity, item.price) # 4. 扣减库存 for item in items: decrease_stock(item.product_id, item.quantity) # 5. 清空购物车中已下单的商品 for item in items: delete_cart_item(user_id, item.product_id) db.execute(COMMIT) except: db.execute(ROLLBACK) raise这段代码的核心就是事务控制。前面讲过SQLite支持事务这里就在实战中充分利用了它。从插入订单到扣库存再清购物车这五步必须是一个原子操作。如果第4步扣库存失败前面插的订单表记录也不能留着否则用户会看到一笔扣了钱但库存没变的订单这就是数据不一致。用生活例子来说事务就像转账从A账户扣钱和往B账户加钱必须同时发生不可能出现A扣了但B没加的情况。如果是用JSON文件存数据这种原子性你根本做不到必须自己写一堆繁琐的异常处理逻辑而且很容易遗漏。5.3 用户角色对业务动作的限制user表的role字段在下单和管理场景中发挥作用。普通用户只能调用跟购物车、下单、查自己订单相关的接口管理员才能操作商品表、处理发货状态、查看所有用户列表。单机项目没有网络层没法通过网关之类的东西统一拦截它用的是最朴实的方式在需要管理权限的函数入口先做角色校验不通过就直接抛异常。虽然代码重复了一些但在单机场景下这是最清晰、最可控的方案而且sqlite的查询效率很高这种权限判断不会成为性能瓶颈。5.4 用户数据与业务数据的隔离思路从设计层面看这个项目始终遵循一个原则用户表只管理账号和身份信息购物车、订单这些业务数据通过user_id跟用户关联但绝不混在同一张表里。这样做的好处是用户系统的变更不会影响业务逻辑反之亦然。比如要给用户表增加一个头像字段只需要动users表订单查询的SQL完全不用改。这种模块解耦在单机小项目里也很重要因为它的代码维护往往靠一个人模块边界清晰能显著减少改联动bug的时间。6. 项目实操中的常见问题与排查技巧说了这么多理论和代码最后来点真正实操中容易碰到的问题。这套用户系统虽然跑起来简单但我实际操作过程中还是踩了几个坑汇总一下也许对你会更有用。6.1 SQLite数据库文件被锁死问题现象程序运行中偶尔会报database is locked错误这通常是因为有另一个进程占用了数据库文件或者同一个程序里多个线程同时执行写操作。排查思路是先确认有没有外部工具打开着数据库文件比如用Navicat、DB Browser之类的工具连着同一个文件就会跟主程序抢占锁。另外在代码里确保所有数据库操作都用短连接模式用完立刻关闭cursor和connection不要长时间保持一个全局连接。如果确实是多线程写频繁可以在连接时设置timeout参数SQLite在一定时间内等不到锁会自动重试比如设置成5秒。实测下来这个方案能解决绝大多数锁冲突问题。6.2 数据库文件损坏或误删后的恢复单机应用有一个天然风险用户可能直接把数据库文件删了或者拷贝到别的机器时漏拷了。这个项目的应对方案很值得参考启动时检测数据库文件是否存在不存在就自动创建并初始化所有表并且内置一个默认的管理员账号。这意味着用户即使把整个数据文件搞丢了程序还能“自愈”重新生成一套干净的初始环境不至于一启动就崩溃。另外如果数据库文件被外部工具写入过不兼容的数据程序启动时可以加一个完整性检查用SQLite的PRAGMA integrity_check来验证。实测约几十毫秒成本很低但能提前发现潜在的文件损坏问题。6.3 忘记密码怎么办单机应用忘记密码是比较常见的使用场景项目提供的解决方案很干脆数据库里保留一个初始管理员账号密码是预设的默认值管理员登录后可以在用户管理界面重置其他用户的密码。因为整个系统是单机本地使用这种“前台重置”的方式反而比邮箱找回更实际、更快。如果你自己二次开发这个项目建议再加一个功能在登录界面放一个“初始化密码”按钮运行后把指定用户名或所有用户的密码重置为默认值这个在本地测试环境里真的很有用。6.4 中文乱码问题SQLite本身对中文支持很好但如果你在Python环境里操作数据库偶尔会出现中文乱码绝大多数情况是Python源文件的编码问题。解决方法是确保Python文件开头声明UTF-8编码并且在写入和读取数据时都统一用UTF-8。SQLite存储的是文本数据本身不关心编码关键是入口的数据编码格式要一致。如果数据库是从其他地方导入的发现中文显示异常可以先检查创建库时有没有用UTF-8编码方式。6.5 用户表数据量变大后的查询优化虽然SQLite在数据量小的时候性能很出色但用户表和订单表的数据量增长到几十万行之后查询性能会开始下降。解决方法是给常用查询字段建索引。这个项目的做法是在users表的username字段上建了唯一索引sessions表的token字段本身就带主键索引cart_items的user_id字段建了普通索引order_items的order_id字段建了索引。建完索引之后只要SQL语句的WHERE条件能命中索引查询速度基本不受数据量影响。另外有一点要小心不要给每一列都建索引因为索引会拖慢写入速度。只给查询频率高、区分度高的列建就足够了。6.6 权限绕过与数据篡改的防御单机项目的威胁模型跟Web项目不一样最大的风险不是远程攻击而是本地的越权操作和文件篡改。比如用户直接改数据库文件把status字段改成1或者把自己的role改成admin普通应用根本防不住。这个项目没有过度设计它默认用户是“可信任但非专业的”也就是用户可能误操作但不会刻意去逆向代码。如果后续要增强安全可以考虑把敏感数据做一层加密存储或者对数据库文件做校验和检查但这些都属于筹码问题的范畴单机场景下做到“密码不泄露、结构清晰”已经够用了。6.7 关键经验分模块测试用户系统我把这套用户系统完全跑通之后最大的实操心得是自动化测试很重要。这个项目的作者在设计用户模块时就为每个核心函数写了单元测试比如测试注册时重复用户名会抛出异常、测试密码校验时错误密码会失败、测试会话过期后get_current_user会返回None。因为用户系统是纯本地函数调用不涉及网络测试起来特别顺手。这一点对于后期功能迭代非常关键每改一次代码跑一遍测试就知道有没有破坏原有功能省掉大量人工回归的时间。7. 从这套用户系统中可以提炼的通用经验把这套用户系统从头到尾看完其实它展示的不只是一个购物商城的局部实现更是一套可以迁移到任何单机应用上的用户体系设计模板。如果你之后想写自己的本地工具、桌面应用、或者离线管理系统下面这几条经验可以直接套用。第一SQLite是本地数据存储的最佳选择之一。它不需要安装服务SQL能力完全够用事务机制保护数据安全唯一的注意点就是别把它当作高并发服务器来用。作为单机应用的数据底座它绰绰有余。第二用户表的设计要面向未来的扩展。哪怕现在只是一个练手项目也要把状态、角色、创建时间这些字段留好否则后面做到深一点的功能时再回头改表代价会高很多。字段宁可多一点不要少一点。第三密码安全必须一步到位。哈希加盐、PBKDF2迭代、恒定时间比较这些实现成本非常低但能避免的隐患极其严重。很多小项目都是因为当年嫌麻烦没做密码保护后来数据泄露变成大事故。第四模块分层是给未来的自己省时间。用户模块、数据访问模块、业务逻辑模块尽量分开哪怕单机项目只有几千行代码清晰的模块边界也能让你三个月后重读代码时更快上手。我个人的体会是很多人在学习技术时只盯着新框架、新特性反而忽略了一个好的用户系统到底该怎么设计。单机应用恰恰是一个绝佳的观察窗口因为没有任何框架替你兜底每一步都能看到底层原理。把这一套用户系统彻底吃透以后再切换到Web、App或者任何其他业务场景你会发现它们的核心逻辑完全相通。希望这篇文章能帮你把一个看似“普通”的单机商城项目读出真正的门道。