1. 项目背景:为什么一个"固定资产系统"会让财务和行政吵起来
固定资产折旧及租赁维修管理系统,名字听着像是个内部OA的小模块,但真做起来会发现,它是典型的"业务规则比技术难"的项目。我接过不少类似的管理系统开发,凡是涉及固定资产的,几乎无一例外会踩到同一个矛盾:财务要的是折旧数据准确、报表可追溯;行政/后勤要的是资产台账清晰、维修记录完整、租赁合同到期有人提醒。两边需求一叠加,技术本身反而不是最难的,难的是把业务规则翻译成数据结构和流程。
这个项目用到的技术栈很明确:Python + Flask 做后端 API,Vue 做前端页面,开发工具用 PyCharm。标题里虽然带了 django 这个词,但核心框架是 Flask,django 更多是作为 Python Web 开发领域经常被一起讨论的对比框架出现。我在做技术选型时,也确实认真对比过 Flask 和 Django,后面会专门讲我的取舍理由。
先把这个系统要解决的核心问题拆开看:
- 固定资产台账管理:资产从入库、领用、调拨到报废的全生命周期记录。
- 折旧计算:按月生成折旧凭证,支持不同折旧方法,并产出报表。
- 租赁管理:资产对外出租时,合同、租金、到期提醒、归还登记。
- 维修管理:资产损坏后的报修、派工、维修费用登记、维保记录。
这套系统的目标用户是中小型企业的行政、财务、资产管理岗,以及接此类定制开发的外包团队。如果你想练手做一个前后端分离的 Flask + Vue 完整项目,或者你刚好接到了一个资产管理类的需求,这篇内容可以作为你的参考蓝本。
我习惯把这类项目称作"表单密集型系统"——没有高并发、没有复杂算法,但充满了状态流转、校验规则和数据一致性要求。做得好的关键,是把你对业务的理解沉淀到数据模型和接口设计里,而不是堆页面。
2. 技术选型:为什么用 Flask + Vue,以及 PyCharm 下的工程搭建
2.1 Flask 与 Django 的取舍
标题里同时出现了 flask 和 django,很容易让人纠结。我在这个项目里最终选了 Flask,理由有三个:
第一,项目规模决定了框架成本。固定资产系统属于典型的中小型管理软件,API 数量大概在 30 到 50 个之间,业务复杂度中等。Flask 的轻量特性让我们可以自由组织目录结构,不受 Django 的"app 应用"约束。Django 自带 Admin、ORM、Migrate 等全家桶能力,但在这种项目里,一半以上的内置功能用不到,反而要花时间绕开它的默认约定。
第二,前后端分离架构下,Flask 只需要专注提供 JSON API。Django 强大的模板系统在前后端分离的模式下没有发挥空间,Flask 的蓝图(Blueprint)机制用来划分模块已经足够清晰。
第三,团队技术栈的延续性。如果团队后续想快速开发小工具或微服务,Flask 的迁移成本更低,代码量更小,心智负担也更轻。
当然,Django 并非不能做。如果你特别依赖 Django Admin 直接生成管理后台,或者项目后续会有复杂的权限体系(Django 自带的 auth 和 permission 确实比 Flask 生态更成熟),选 Django 完全合理。这个项目里我选 Flask,是因为我更看重接口的灵活性和代码的可读性。
2.2 前端用 Vue 的原因
Vue 在这类管理系统中几乎是标配,原因很简单:
- 数据双向绑定让表单开发效率极高,资产录入页面的字段动辄二十多个,用原生 JS 操作 DOM 会写到怀疑人生。
- Element UI 或 Element Plus 组件库自带表格、表单、日期选择器、弹窗,和后台管理系统的界面需求高度匹配。
- Vue 的生态对新手友好,教程多、踩坑解决方案多,团队招人也好招。
前后端分离的模式下,Vue 跑在 8080 端口,Flask 跑在 5000 端口,二者通过 HTTP 通信。开发阶段用 Vite 或 Vue CLI 的代理转发解决跨域,生产环境用 Nginx 统一入口。这个架构可以说是目前中小型管理系统的标准答案。
2.3 PyCharm 下的工程目录规划
PyCharm 作为 Python 的 IDE,对这个项目最大的帮助在于虚拟环境管理、调试断点和数据库工具面板。我习惯在 PyCharm 里新建项目时直接创建 venv 虚拟环境,配合 requirements.txt 做依赖管理。
后端目录结构建议这样组织:
asset_management/ ├── app.py # 应用入口 ├── config.py # 配置文件 ├── models/ │ ├── __init__.py │ ├── asset.py # 资产模型 │ ├── depreciation.py # 折旧记录模型 │ ├── lease.py # 租赁模型 │ └── repair.py # 维修模型 ├── api/ │ ├── __init__.py │ ├── asset_api.py # 资产相关接口 │ ├── depreciation_api.py # 折旧相关接口 │ ├── lease_api.py # 租赁相关接口 │ └── repair_api.py # 维修相关接口 ├── utils/ │ ├── auth.py # JWT 鉴权 │ └── response.py # 统一响应格式 ├── requirements.txt └── run.py前端单独建一个 Vue 项目,按 views、components、api、router 四个目录组织。这里有一个我踩过坑后的建议:前后端项目不要放在同一个目录下混合管理,而是同级放,比如backend/和frontend/两个目录并列。否则 Git 提交时很容易误操作,而且 PyCharm 打开混合目录时索引速度会明显变慢。
3. 数据库建模:资产、折旧、租赁、维修四张表的血缘关系
3.1 从资产卡片反推字段设计
固定资产系统的核心是"资产卡片",所有业务都围绕资产 ID 展开。我在设计数据表时,不是直接上手建表,而是先模拟用户录入一张资产卡片的全过程,列出所有必填和选填字段。
一张典型的资产卡片包含:
- 资产基础信息:资产编码、名称、分类(办公设备/生产设备/运输工具/房屋建筑等)、规格型号、供应商、存放地点、使用部门、保管人。
- 财务信息:原值、预计净残值率、预计使用年限、入账日期、折旧方法。
- 状态信息:当前状态(在库/在用/出租/维修中/已报废)、状态变更时间。
对应到数据库模型,我设计了Asset表:
class Asset(db.Model): __tablename__ = 'asset' id = db.Column(db.Integer, primary_key=True) asset_code = db.Column(db.String(50), unique=True, nullable=False, index=True) name = db.Column(db.String(100), nullable=False) category = db.Column(db.String(50), nullable=False) # 资产分类 spec = db.Column(db.String(100)) # 规格型号 supplier = db.Column(db.String(100)) location = db.Column(db.String(100)) department = db.Column(db.String(50)) custodian = db.Column(db.String(50)) # 保管人 original_value = db.Column(db.Numeric(12, 2), nullable=False) # 原值 residual_rate = db.Column(db.Float, default=0.05) # 预计净残值率 useful_life = db.Column(db.Integer, nullable=False) # 预计使用年限(月) purchase_date = db.Column(db.Date, nullable=False) # 入账日期 depreciation_method = db.Column(db.String(20), default='straight_line') # 折旧方法 status = db.Column(db.String(20), default='in_stock') # 资产状态 created_at = db.Column(db.DateTime, default=datetime.utcnow)这里有两个容易被忽视的设计细节:
资产编码必须唯一且带索引。资产管理最频繁的操作就是根据编码查资产,这个字段不建索引,数据量过万后接口响应会明显变慢。
金额字段用Numeric(12, 2)而不是Float。财务数据最忌讳浮点精度问题,0.1 + 0.2 的经典错误在折旧累计计算中会被放大。Numeric在 SQLAlchemy 中对应数据库的 DECIMAL 类型,能精确控制小数位。
3.2 折旧记录表:为什么要单独建一张表而不是只算一个字段
很多初学者会把"累计折旧"设计成 Asset 表里的一个字段,每月更新一次。这种设计的最大问题是:你丢失了历史。三个月后想查"某资产某月折旧了多少",数据已经覆盖掉了。
正确的做法是单独建DepreciationRecord表,每条记录代表某个资产某个月的折旧情况:
class DepreciationRecord(db.Model): __tablename__ = 'depreciation_record' id = db.Column(db.Integer, primary_key=True) asset_id = db.Column(db.Integer, db.ForeignKey('asset.id'), nullable=False, index=True) period = db.Column(db.String(7), nullable=False) # 折旧期间,格式:2025-06 depreciation_amount = db.Column(db.Numeric(12, 2), nullable=False) # 本月折旧额 cumulative_depreciation = db.Column(db.Numeric(12, 2), nullable=False) # 累计折旧 net_value = db.Column(db.Numeric(12, 2), nullable=False) # 净值 created_at = db.Column(db.DateTime, default=datetime.utcnow) asset = db.relationship('Asset', backref=db.backref('depreciation_records', lazy='dynamic')) __table_args__ = (db.UniqueConstraint('asset_id', 'period', name='uniq_asset_period'),)asset_id和period加了联合唯一约束,这是为了保证同一个资产在同一个月不会生成两条折旧记录。批量折旧生成时如果重复执行,这个约束就是最后一道防线。
3.3 租赁与维修的状态字段设计
租赁表的核心是追踪合同状态。我设计了LeaseContract表,字段包括合同编号、资产 ID、承租方、租赁开始日期、结束日期、月租金、押金、合同状态(生效中/已到期/已终止)、备注等。
维修表RepairRecord则围绕工单流转来设计:报修人、报修日期、故障描述、维修方、维修类型(内部/外部)、预计费用、实际费用、维修开始日期、完成日期、维修结果、验收状态。
这两张表有一个共同的设计要点:状态字段不要用自由字符串,而是用常量枚举。我在项目里定义了一个AssetStatus类:
class AssetStatus: IN_STOCK = 'in_stock' # 在库 IN_USE = 'in_use' # 在用 LEASED = 'leased' # 出租中 REPAIRING = 'repairing' # 维修中 SCRAPPED = 'scrapped' # 已报废业务规则里有大量"当资产处于某个状态时,才能执行某个操作"的约束。比如:资产已报废就不能再发起报修;资产出租期间不能调拨。这些规则看似简单,但散落在代码里就会变成一个个 if 判断,一旦状态多了管理起来非常痛苦。我的做法是把状态流转集中到一个工具函数里校验:
ALLOWED_TRANSITIONS = { AssetStatus.IN_STOCK: [AssetStatus.IN_USE, AssetStatus.LEASED, AssetStatus.SCRAPPED], AssetStatus.IN_USE: [AssetStatus.IN_STOCK, AssetStatus.REPAIRING, AssetStatus.SCRAPPED], AssetStatus.LEASED: [AssetStatus.IN_STOCK, AssetStatus.REPAIRING], AssetStatus.REPAIRING: [AssetStatus.IN_USE, AssetStatus.IN_STOCK], AssetStatus.SCRAPPED: [], } def validate_transition(asset, target_status): allowed = ALLOWED_TRANSITIONS.get(asset.status, []) if target_status not in allowed: raise ValueError(f'资产状态不允许从 {asset.status} 变更为 {target_status}')这个配置表的好处是,业务规则一目了然,测试也好写。后续如果要加状态(比如"借用中"),只需要在配置里加一行。
4. 折旧计算模块:业务规则和代码实现
4.1 四种折旧方法的业务理解
固定资产折旧,会计上有几种常用方法,这个系统里我实现了其中两种最常用的:直线法和双倍余额递减法。其他方法(工作量法、年数总和法)其实也是公式变换,理解了核心逻辑后加代码很容易。
直线法的公式是:
月折旧额 = (原值 - 预计净残值) / 预计使用年限(月)预计净残值 = 原值 × 预计净残值率。大多数企业会设置 5% 的残值率,也就是说一台原值 12000 元的设备,最终要折旧的总额是 11400 元。
双倍余额递减法则是加速折旧,前期折旧多、后期少。公式是:
年折旧率 = 2 / 预计使用年限(年)但在实际开发中,纯按公式算会出现一个问题:最后一个月的净值会被扣成负数。所以需要在净值低于某个阈值时切换到直线法,把剩余净值在剩余月份内平均摊销。这也是"双倍余额递减法后期改直线法"这个会计惯例的由来。
4.2 直线法折旧的 Python 实现
我写了一个calculate_depreciation函数,入参是资产信息和折旧月份,返回该月应提折旧:
def calculate_straight_line_depreciation(asset, period): original_value = float(asset.original_value) residual_value = original_value * asset.residual_rate depreciable_base = original_value - residual_value monthly_amount = round(depreciable_base / asset.useful_life, 2) return monthly_amount这里有个细节:useful_life存的是月份数。比如预计使用年限 5 年,就存 60。如果存的是年数,计算时就要乘 12,容易在边界判断上出问题。
但仅仅一个函数不够,因为资产不是买了当月就开始折旧的。按会计准则,固定资产的折旧通常从入账的次月开始计提。所以生成折旧记录时需要判断:如果purchase_date所在的月份等于period,则这个月不生成记录,从下个月开始。
4.3 批量生成折旧记录的完整逻辑
实际业务中不会一台台资产去算折旧,而是每月末跑一次批量任务,把当时状态为"在库"和"在用"的资产全部扫描一遍,生成当月折旧。
核心代码如下:
def generate_monthly_depreciation(period): assets = Asset.query.filter( Asset.status.in_([AssetStatus.IN_STOCK, AssetStatus.IN_USE]) ).all() created_count = 0 for asset in assets: # 判断是否已计提完毕 last_record = DepreciationRecord.query.filter_by(asset_id=asset.id)\ .order_by(DepreciationRecord.period.desc()).first() if last_record and float(last_record.net_value) <= 0: continue # 判断是否已生成过该月记录 existing = DepreciationRecord.query.filter_by( asset_id=asset.id, period=period ).first() if existing: continue monthly_amount = calculate_straight_line_depreciation(asset, period) cumulative = (last_record.cumulative_depreciation + monthly_amount if last_record else monthly_amount) net_value = float(asset.original_value) - cumulative # 最后一期修正:避免负数净值 if net_value < 0: net_value = 0 monthly_amount = float(asset.original_value) - float(last_record.cumulative_depreciation) record = DepreciationRecord( asset_id=asset.id, period=period, depreciation_amount=monthly_amount, cumulative_depreciation=cumulative, net_value=net_value ) db.session.add(record) created_count += 1 db.session.commit() return created_count这里的最后一期修正逻辑非常关键。因为monthly_amount四舍五入到分之后,最后一期的累计折旧不一定恰好等于应折旧总额,可能多一分也可能少一分。所以要在净值小于 0 时反向修正当月折旧额。
4.4 折旧报表的接口实现
前端需要一个趋势图或者列表来展示每个月折旧总额。对应接口的 SQL 很简单:
records = db.session.query( DepreciationRecord.period, db.func.sum(DepreciationRecord.depreciation_amount).label('total_depreciation') ).group_by(DepreciationRecord.period).all()但这里我踩过一个坑:period 字段是字符串类型时,排序会按字典序而不是时间序。比如'2025-10'会排在'2025-9'前面,导致前端折线图的横轴错乱。解决方式有两个:一是把 period 存成 Date 类型,只在展示时格式化成YYYY-MM;二是查询时按period排序后,在 Python 里用sorted(records, key=lambda r: r.period)按字典序排确实坑,因为'2025-9'和'2025-10'字典序上'2025-10'更靠前,但'2025-9'后面没有'10'所以其实没问题,反而是'2025-1'到'2025-9'的排序会乱。我最终选择了把 period 转为YYYY-MM-DD的 Date 类型(固定每月第一天),这样排序不会出错。
5. 租赁与维修管理:状态机为核心的业务闭环
5.1 租赁管理的完整流程设计
租赁业务看着简单,其实涉及多个环节:
- 登记租赁合同(选资产、填承租方、租期、租金)
- 合同生效时,资产状态从"在库"变为"出租中"
- 每个月可以登记租金收款记录
- 租赁到期前自动提醒(看板展示即将到期的合同)
- 租期结束办理归还,资产状态回到"在库"
在这个流程里,最容易出问题的环节是"资产状态同步"。假设资产 ID 10086 被租赁了,合同创建成功但资产状态没改,后面别人就可能再次把这个资产租出去。我的做法是,在同一个事务里完成合同创建和资产状态变更:
def create_lease_contract(data): asset = Asset.query.get(data['asset_id']) if asset.status != AssetStatus.IN_STOCK: raise ValueError('该资产当前不可租赁') contract = LeaseContract(**data) db.session.add(contract) asset.status = AssetStatus.LEASED db.session.commit() return contract这样设计的好处是,数据库事务的原子性保证了"合同存在且资产被占用"这两个事实永远同时成立。
5.2 维修工单的生命周期管理
维修流程更加琐碎。一个报修工单从创建到归档,需要经历:
- 报修登记:填写资产、故障描述、报修人。
- 派单:指定维修方(内部人员或外部供应商),填写预计费用。
- 维修中:维修方执行维修,可更新进度备注。
- 验收:资产使用人确认维修结果,填写实际费用。
- 归档:工单关闭,资产状态恢复。
对应到代码里,RepairRecord表有一个stage字段,取值范围是pending(待派单)、repairing(维修中)、completed(已完成)、closed(已归档)。前端每个操作按钮的显隐,完全由当前stage决定。
5.3 Vue 前端如何与后端接口联动
以报修功能为例,前端页面流程是:
- 在资产列表页点击"报修"按钮,弹窗显示报修表单。
- 表单提交到
POST /api/repair,后端返回工单 ID。 - 工单列表页根据
stage展示不同操作按钮。
这里我想强调一个前后端协作的细节:后端返回的状态码和数据结构必须统一格式。我在utils/response.py里封装了统一的响应格式:
def success(data=None, message='操作成功'): return {'code': 0, 'message': message, 'data': data} def error(message='操作失败', code=400): return {'code': code, 'message': message, 'data': None}前端在api/request.js里封装 axios 拦截器,统一处理code:
service.interceptors.response.use( (response) => { const res = response.data if (res.code !== 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, (error) => { ElMessage.error(error.response?.data?.message || '网络错误') return Promise.reject(error) } )这样前端业务代码里不用到处写 try-catch,报错提示也统一了。很多小项目的接口返回格式五花八门——有返回裸对象的、有包一层的、错误提示有的在message有的在msg——联调的时候会让你崩溃到怀疑人生。
5.4 到期提醒和待办看板的实现
这个系统有个很实用的功能:首页看板。展示三类信息:
- 本月即将到期的租赁合同(结束日期在 30 天内且状态为生效中)
- 待派单的维修工单
- 本月折旧总额
对应的接口:
@app.route('/api/dashboard/summary') def dashboard_summary(): today = date.today() thirty_days_later = today + timedelta(days=30) expiring_leases = LeaseContract.query.filter( LeaseContract.end_date >= today, LeaseContract.end_date <= thirty_days_later, LeaseContract.status == 'active' ).count() pending_repairs = RepairRecord.query.filter_by(stage='pending').count() current_month = today.strftime('%Y-%m') total_depreciation = db.session.query( db.func.sum(DepreciationRecord.depreciation_amount) ).filter(DepreciationRecord.period == current_month).scalar() or 0 return success({ 'expiring_leases': expiring_leases, 'pending_repairs': pending_repairs, 'total_depreciation': float(total_depreciation) })前端拿到这三个数字,用 Element UI 的统计卡片展示即可。这种看板功能实现成本极低,但给用户的体验提升非常明显——不用自己翻列表去数快到期的是哪几份合同。
6. 联调阶段最容易踩的坑:跨域、鉴权、文件上传
6.1 跨域问题:Flask-CORS 的配置与踩坑
前后端分离开发时,Vue 跑在 8080,Flask 跑在 5000,浏览器会拦截跨域请求。最常见的解决方案是安装flask-cors:
from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": "http://localhost:8080"}})这个配置看似简单,但有一个容易忽略的问题:如果前端请求带了自定义 Header(比如 Authorization),后端必须在 CORS 配置里允许该 Header:
CORS(app, resources={r"/api/*": { "origins": "http://localhost:8080", "allow_headers": ["Content-Type", "Authorization"], "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"] }})否则前端会报CORS header 'Access-Control-Allow-Headers' is missing。这个问题排查起来非常迷惑,因为明明配置了 CORS 却还是跨域报错。
不过,我在开发阶段一般不用 Flask-CORS,而是用 Vue CLI 的 devServer 代理。配置vue.config.js如下:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } }这样前端请求/api/xxx时,开发服务器会把请求转发到 Flask,浏览器看到的是同源请求,完全绕过了 CORS。这个方案在生产环境用 Nginx 反向代理同样适用,所以代码里不需要为 CORS 做任何特殊处理。
6.2 JWT 鉴权与接口权限控制
管理系统的接口不能裸奔,需要登录鉴权。我用的方案是 JWT(JSON Web Token),流程是:
- 用户输入用户名密码,后端验证后签发 Token。
- 前端把 Token 存到 localStorage。
- 后端提供一个
@token_required装饰器,校验请求头里的 Authorization。
Flask 里实现 JWT,可以直接用pyjwt库,不需要引入flask-jwt-extended这种重框架(虽然它确实更方便)。
import jwt from functools import wraps from flask import request, jsonify SECRET_KEY = 'your-secret-key' def token_required(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization') if not token: return jsonify({'code': 401, 'message': '未提供认证令牌'}), 401 try: if token.startswith('Bearer '): token = token[7:] payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) request.user_id = payload['user_id'] except jwt.ExpiredSignatureError: return jsonify({'code': 401, 'message': '登录已过期'}), 401 except jwt.InvalidTokenError: return jsonify({'code': 401, 'message': '无效的认证令牌'}), 401 return f(*args, **kwargs) return decorated这里有一个安全细节:JWT 的 SECRET_KEY 一定不要硬编码在代码里,应该从环境变量或配置文件中读取。项目被 Git 托管时,如果你不小心把密钥提交上去,任何人都可以伪造 Token 访问你的接口。
权限控制层面,我的建议是不要过度设计。先想清楚系统里有哪几类角色,比如:资产管理员(可以增删改查资产)、财务(可以执行折旧计算和查看报表)、普通员工(只能查看资产信息并发起报修)。在token_required的基础上再加一个role_required装饰器,传入允许的角色列表,拦截没有权限的请求即可。
6.3 资产图片上传与静态文件服务
资产卡片经常需要上传资产照片,这里会遇到两个问题:
一是 Flask 默认的静态文件夹是static/,前端上传文件时必须让后端知道文件存放路径。我习惯把上传目录配置在 config 里:
class Config: UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads') ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}二是 Flask 在开发环境处理文件上传的方式:
@app.route('/api/upload', methods=['POST']) @token_required def upload_file(): file = request.files.get('file') if not file: return error('未收到文件') if not allowed_file(file.filename): return error('文件类型不支持') filename = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}" file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename)) return success({'url': f'/uploads/{filename}'})注意secure_filename这个函数非常重要,它可以过滤掉文件名中的路径分隔符和特殊字符,防止路径穿越攻击。
生产环境下,/uploads目录不应该由 Flask 直接服务,而是交给 Nginx 做静态文件映射。配置如下:
location /uploads/ { alias /path/to/asset_management/uploads/; }7. 测试与部署:保证系统真正能交给用户用
7.1 核心业务逻辑的单元测试
这种管理系统虽然没有算法复杂度,但折旧计算这种纯逻辑函数很适合做单元测试。我用 pytest 写了几个核心测试用例:
def test_straight_line_depreciation(): asset = Asset( original_value=12000, residual_rate=0.05, useful_life=60 ) amount = calculate_straight_line_depreciation(asset, '2025-06') # 12000 * 0.95 / 60 = 190.0 assert amount == 190.0 def test_last_period_no_negative_net_value(): # 模拟折旧到只剩 100 元净值的场景 # 验证修正逻辑不会产生负数净值 pass测试的价值不在于现在,而在于未来。当你后来改了折旧逻辑(比如新增了一种折旧方法),跑一遍测试就能确认原有业务没被破坏。
7.2 用 Gunicorn + Nginx 部署
Flask 自带的开发服务器(app.run())绝对不能用于生产环境,它既慢又不安全。我用的方案是 Gunicorn 做 WSGI 服务器,Nginx 做反向代理。
Gunicorn 启动命令:
gunicorn -w 4 -b 127.0.0.1:5000 run:app-w 4表示启动 4 个工作进程。对于这个规模的管理系统,4 个 worker 足够支撑几十人同时使用。
Nginx 配置:
server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { root /path/to/frontend/dist; try_files $uri $uri/ /index.html; } }这里try_files ... /index.html是 Vue Router 使用 history 模式时的标配,保证前端路由刷新时不会 404。
7.3 交付后的维护建议
固定资产系统这类项目,交付不是结束,而是维护的开始。根据我的经验,有几点值得注意:
- 备份策略:SQLite 或 MySQL 的备份要定期自动化,固定资产数据的丢失对企业来说是重大事故。
- 数据导入:用户大概率有存量资产数据需要批量导入,一定要做一个 Excel 导入功能,否则手动录入几百条资产记录会让他们直接放弃使用。
- 审计日志:记录谁在什么时候修改了哪条资产记录。很多企业有审计需求,这个功能在需求阶段可能没提,但上线后大概率会补。
我个人在实际项目里体会最深的一点是:这类系统真正的技术难点从来不是某个框架的用法,而是如何把你对业务的理解,准确翻译成数据模型和状态流转规则。Flask、Vue 这些工具,熟练之后都只是手段。所以如果你正准备做类似的项目,建议花 30% 的时间搞懂业务,30% 的时间设计数据库,剩下的时间写代码,你会发现整个过程顺畅很多。