零基础入门python21:Flask账本的数据模型——用户、分类和账目 零基础入门python21Flask账本的数据模型——用户、分类和账目一、上一篇课后练习讲解上一篇要求使用环境变量并验证测试配置。参考测试通过create_app({TESTING: True, SQLALCHEMY_DATABASE_URI: sqlite:///:memory:})创建临时应用再在应用上下文中建表。生产配置永远不应写进测试文件。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点Config 与 TestConfig 分离环境变量只在配置层读取测试使用内存 SQLite蓝图和扩展仍由 create_app 装配。文件app/config.py完整参考答案文件完整文件app/config.pyimportosclassConfig:APP_VERSIONos.getenv(APP_VERSION,0.1.0)SECRET_KEYos.getenv(SECRET_KEY,dev-only-change-me)SQLALCHEMY_DATABASE_URIos.getenv(DATABASE_URL,sqlite:///ledger.db)SQLALCHEMY_TRACK_MODIFICATIONSFalseclassTestConfig(Config):TESTINGTrueSQLALCHEMY_DATABASE_URIsqlite:///:memory:完整参考答案文件本篇对应的交付源码完整文件flask-ledger/app/init.pyfromflaskimportFlaskfrom.authimportbpasauth_bpfrom.extensionsimportdb,login_managerfrom.ledgerimportbpasledger_bpdefcreate_app(test_configNone):appFlask(__name__)app.config.from_mapping(SECRET_KEYdev-change-me,SQLALCHEMY_DATABASE_URIsqlite:///ledger.db,SQLALCHEMY_TRACK_MODIFICATIONSFalse,)iftest_config:app.config.update(test_config)db.init_app(app)login_manager.init_app(app)login_manager.unauthorized_handler(lambda:({message:请先登录},401))app.register_blueprint(auth_bp)app.register_blueprint(ledger_bp)app.get(/api/health)defhealth():return{status:ok}withapp.app_context():db.create_all()returnapp验收命令python -m pytest -qDjango 项目使用 python manage.py test。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇完成什么建立账本的三类数据用户拥有多个分类和账目账目必须属于一个分类。金额用Numeric(12, 2)不能用浮点数。三、模型代码classUser(UserMixin,db.Model):iddb.Column(db.Integer,primary_keyTrue)emaildb.Column(db.String(120),uniqueTrue,nullableFalse,indexTrue)password_hashdb.Column(db.String(255),nullableFalse)classCategory(db.Model):__table_args__(db.UniqueConstraint(user_id,name),)iddb.Column(db.Integer,primary_keyTrue)namedb.Column(db.String(40),nullableFalse)user_iddb.Column(db.Integer,db.ForeignKey(user.id),nullableFalse,indexTrue)classTransaction(db.Model):iddb.Column(db.Integer,primary_keyTrue)kinddb.Column(db.String(10),nullableFalse)amountdb.Column(db.Numeric(12,2),nullableFalse)user_iddb.Column(db.Integer,db.ForeignKey(user.id),nullableFalse,indexTrue)category_iddb.Column(db.Integer,db.ForeignKey(category.id),nullableFalse)联合唯一约束让同一用户不能创建两个同名分类它必须在数据库中实现因为两个并发请求可能同时通过 Python 的“先查询再新增”。外键保证账目不能指向不存在的用户或分类。四、迁移与验收教学项目先用db.create_all()初始化测试库生产项目应使用 Flask-Migrate 记录迁移。运行测试后检查负金额不会由模型自动变合法业务层还需要校验kind和金额范围。课后练习给 Transaction 增加happened_on日期字段并写一个按日期查询的测试。项目增量三个核心模型账本需要 User、Category、Entry 三个模型。Entry 的 user_id 是数据隔离关键金额使用 Numeric 而不是 Float日期字段用于月度报表。classEntry(db.Model):iddb.Column(db.Integer,primary_keyTrue)user_iddb.Column(db.ForeignKey(user.id),nullableFalse,indexTrue)category_iddb.Column(db.ForeignKey(category.id),nullableFalse)amountdb.Column(db.Numeric(12,2),nullableFalse)happened_ondb.Column(db.Date,nullableFalse)迁移后插入收入和支出查询时必须带当前用户过滤。课后练习增加数据库约束并验证非法金额。五、从业务名词到表结构先画关系再写模型账本不是“把一行 JSON 塞进数据库”。用户、分类、账目之间有明确关系一个用户拥有多条账目和分类一条账目可以没有分类但不能没有所属用户。把关系写成表格后再编码能够提前发现“删除用户时账目怎么办”这类问题。user (1) ─── category (0..n) │ └─────── entry (1..n) ─── category (0..1)对应的模型示例项目实际代码还包含时间字段和索引classEntry(db.Model):__tablename__entriesiddb.Column(db.Integer,primary_keyTrue)user_iddb.Column(db.Integer,db.ForeignKey(users.id),nullableFalse,indexTrue)category_iddb.Column(db.Integer,db.ForeignKey(categories.id),nullableTrue)amountdb.Column(db.Numeric(12,2),nullableFalse)happened_ondb.Column(db.Date,nullableFalse,indexTrue)notedb.Column(db.String(200),nullableFalse,default)Numeric(12, 2)表示最多 12 位、2 位小数金额不能用Float因为二进制浮点会出现0.1 0.2 ! 0.3的舍入误差。user_id加索引是因为之后的每一个列表查询都会按用户过滤。category_id允许为空表示用户暂时不分类但user_id必须存在这是领域规则而非界面约束。六、迁移和回滚的最小流程开发阶段可以使用db.create_all()但多人协作和生产发布必须使用迁移工具。每次模型改变都要记录“从哪个版本变到哪个版本”flask--app run.py db init# 只在项目第一次执行flask--app run.py db migrate-madd entry and categoryflask--app run.py db upgrade flask--app run.py db current执行migrate后一定要打开生成的迁移文件检查 SQL自动生成器不会理解“把字段改名”与“删掉旧字段”的业务含义。发布失败时使用flask db downgrade -1回退一个版本先恢复服务再分析迁移脚本。七、上一篇练习的实现和数据隔离同一用户的同名分类唯一约束必须包含user_id而不是只对name加全局唯一classCategory(db.Model):__table_args__(db.UniqueConstraint(user_id,name,nameuq_category_user_name),)API 查询也必须带用户条件Category.query.filter_by(idcategory_id, user_idcurrent_user.id).first_or_404()。只按id查出来再判断所有权虽然看似可行却很容易在另一个接口忘记判断把所有权放进 SQL 条件是更可靠的默认行为。八、验收输出与排错python-cfrom app import create_app; from app.extensions import db; appcreate_app({TESTING:True}); app.app_context().push(); print([t.name for t in db.metadata.sorted_tables])python-m pytest-q预期表名包含users、categories、entries。NoReferencedTableError表示外键字符串与__tablename__不一致UNIQUE constraint failed说明约束生效接口层应把它转成 409而不是返回 500。下篇会实现注册并把数据库异常转换为用户能看懂的错误。九、本篇练习写一个seed_demo()函数创建一个用户、两个分类和三条账目函数重复执行不得产生重复数据。要求使用事务失败时回滚。下一篇开始注册接口会复用这个事务边界。本篇结束完整模块文件下面是交付项目中真实存在的完整文件 flask-ledger/app/models.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码复制单个函数会丢失上下文因此这里提供整份文件。fromdatetimeimportdate,datetime,timezonefromdecimalimportDecimalfromflask_loginimportUserMixinfromwerkzeug.securityimportcheck_password_hash,generate_password_hashfrom.extensionsimportdb,login_managerclassUser(UserMixin,db.Model):iddb.Column(db.Integer,primary_keyTrue)emaildb.Column(db.String(120),uniqueTrue,nullableFalse,indexTrue)password_hashdb.Column(db.String(255),nullableFalse)created_atdb.Column(db.DateTime,defaultlambda:datetime.now(timezone.utc))defset_password(self,password:str)-None:self.password_hashgenerate_password_hash(password)defcheck_password(self,password:str)-bool:returncheck_password_hash(self.password_hash,password)login_manager.user_loaderdefload_user(user_id:str):returndb.session.get(User,int(user_id))classCategory(db.Model):__table_args__(db.UniqueConstraint(user_id,name),)iddb.Column(db.Integer,primary_keyTrue)namedb.Column(db.String(40),nullableFalse)user_iddb.Column(db.Integer,db.ForeignKey(user.id),nullableFalse,indexTrue)classTransaction(db.Model):iddb.Column(db.Integer,primary_keyTrue)kinddb.Column(db.String(10),nullableFalse)amountdb.Column(db.Numeric(12,2),nullableFalse)notedb.Column(db.String(200),default)happened_ondb.Column(db.Date,defaultdate.today,nullableFalse,indexTrue)user_iddb.Column(db.Integer,db.ForeignKey(user.id),nullableFalse,indexTrue)category_iddb.Column(db.Integer,db.ForeignKey(category.id),nullableFalse)defto_dict(self)-dict:return{id:self.id,kind:self.kind,amount:str(Decimal(self.amount).quantize(Decimal(0.01))),note:self.note,happened_on:self.happened_on.isoformat(),category_id:self.category_id,}

相关新闻

最新新闻

Microsoft |PowerToys 源码静态审阅:从 5418 个文件看 Windows 工具集的模块化设计与工程治理

Microsoft |PowerToys 源码静态审阅:从 5418 个文件看 Windows 工具集的模块化设计与工程治理

Microsoft |PowerToys 源码静态审阅:从 5418 个文件看 Windows 工具集的模块化设计与工程治理评测对象: PowerToys 开源源码 评测类型:证据驱动的只读静态工程审阅 评测边界:本文未执行项目构建、测试、依赖扫描或运行…

2026/8/25 13:59:42
2026年读论文的 AI 工具哪个好用:快速筛选、深度问答、免费入门分别选哪款

2026年读论文的 AI 工具哪个好用:快速筛选、深度问答、免费入门分别选哪款

📌 TL;DR已经决定用 AI 读论文了,但刷到一堆"神器"推文,每篇都说好用,却不告诉你适合谁。自己下了五六个——有的要付费、有的要魔法、有的只有英文界面,最后不知道该留哪个。问题不在工具,在于你…

2026/8/25 13:59:42
答辩前3天,PPT还没做,我是怎么扛过来的

答辩前3天,PPT还没做,我是怎么扛过来的

相信很多同学都有过这种窒息时刻:毕业答辩倒计时3天,论文终稿刚改完,答辩PPT一片空白,思路混乱、无从下手,熬夜焦虑却毫无进展。前段时间我的毕业答辩就是这个状态。前期忙着改论文、调格式、降重,硬生生把…

2026/8/25 13:59:42
三张财务报表怎么联动?很多财务工作多年都没真正搞懂

三张财务报表怎么联动?很多财务工作多年都没真正搞懂

做财务时间久了,会发现一个很有意思的现象:资产负债表、利润表、现金流量表几乎天天接触。但真要解释为什么利润涨了现金却少了、为什么经营现金流变好了利润却没怎么增长、为什么收入越做越大反而越来越缺钱,不少人还是要重新把三张表翻出来…

2026/8/25 13:59:42
基于SpringBoot的二手车交易平台系统(源码+lw+部署文档+讲解等)

基于SpringBoot的二手车交易平台系统(源码+lw+部署文档+讲解等)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/25 13:59:42
基于SpringBoot的瑜伽馆网站的设计与实现源码+文档

基于SpringBoot的瑜伽馆网站的设计与实现源码+文档

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/25 13:54:42