
对于刚毕业的大学生或者对生活精打细算的人来讲记录自己的生活消费支出可以更好的管理资金流闲言少絮我来分享一个这么一个生活消费可视化的程序data文件夹(删除序号使用)1.entertainment.json {records:[]} 2.income.json { records: [ { date: 2026-06, amount: 0.0 } ] } 3.living.json {records:[]} 4.utility.json {records:[]}services文件夹1.__init__.py 空 2.storage.py # -*- coding: utf-8 -*- # 封装 JSON 文件读写和数据操作 import json import os def load_json(filepath): 加载 JSON 文件为 Python 字典, 若文件不存在则初始化 if not os.path.exists(filepath): # 文件不存在时初始化空结构 if utility.json in filepath: obj {records: [], unit_price: {electric: 0.68, water: 3.00}} else: obj {records: []} save_json(filepath, obj) return obj with open(filepath, r, encodingutf-8) as f: return json.load(f) def save_json(filepath, obj): 保存 Python 字典到 JSON 文件 with open(filepath, w, encodingutf-8) as f: json.dump(obj, f, ensure_asciiFalse, indent2) def append_record(filepath, rec): 向 records 列表追加新记录(若存在日期则覆盖) data load_json(filepath) rec_date rec.get(date) for idx, item in enumerate(data.get(records, [])): if item.get(date) rec_date: data[records][idx] rec break else: data.setdefault(records, []).append(rec) save_json(filepath, data) def delete_record(filepath, date): 删除指定日期的记录 data load_json(filepath) data[records] [r for r in data.get(records, []) if r.get(date) ! date] save_json(filepath, data)static文件夹包含css文件夹和js文件夹css文件夹style.css body { font-family: Microsoft YaHei,Arial,Verdana,sans-serif; } h2 { margin-bottom: 17px; color: #2460a1; font-weight: 600; } .table th, .table td { vertical-align: middle; } .btn-primary { background:#2460a1; border-color:#2460a1;} .btn-info { background:#fffde7; color:#2460a1;border-color:#2460a1;} .navbar { font-size: 1.07rem; } .card { box-shadow: 0 2px 5px #dbeafe1c; } .container { max-width:900px; }js文件夹1.entertainment.js function renderEntertainmentChart(data) { let dates data.map(rr.date); let amounts data.map(rr.amount); let chart echarts.init(document.getElementById(entertainment-chart)); chart.setOption({ tooltip: { trigger:axis }, xAxis: { type:category, data:dates }, yAxis: { type:value}, series: [{ name: 娱乐消费, data: amounts, type: bar, itemStyle: { color: #43a047 }, }] }); } 2.living.js function renderLivingChart(data) { let dates data.map(rr.date); let amounts data.map(rr.amount); let chart echarts.init(document.getElementById(living-chart)); chart.setOption({ tooltip: { trigger:axis }, xAxis: { type:category, data:dates }, yAxis: { type:value}, series: [{ name: 生活消费, data: amounts, type: bar, itemStyle: { color: #ffa000 }, }] }); } 3.overview.js function renderOverviewCharts(utilRecs, elePrice, watPrice, ent, liv, income) { let today new Date(); let ym today.getFullYear() - String(today.getMonth()1).padStart(2,0); let getMonth d d.slice(0,7); let incomeM 0; for(let i0;iincome.length;i) if(income[i].dateym) incomeM income[i].amount; // 本月水电 let uMonth utilRecs.filter(dgetMonth(d.date)ym); let eMonth ent.filter(dgetMonth(d.date)ym); let lMonth liv.filter(dgetMonth(d.date)ym); let eleSum0, watSum0; for(let i1;iuMonth.length;i){ let eu uMonth[i].electric-uMonth[i-1].electric; let wu uMonth[i].water-uMonth[i-1].water; eleSum eu*elePrice; watSum wu*watPrice; } let entSum eMonth.reduce((a,b)ab.amount,0); let livSum lMonth.reduce((a,b)ab.amount,0); let total eleSumwatSumentSumlivSum, balanceincomeM-total; let rate incomeM0 ? (balance/incomeM*100).toFixed(1) : 0; $(#overview-cards).html( div classcol-md-4div classcarddiv classcard-body本月总支出span classfw-bold${total.toFixed(2)}/span 元/div/div/div div classcol-md-4div classcarddiv classcard-body本月结余span classfw-bold${balance.toFixed(2)}/span 元/div/div/div div classcol-md-4div classcarddiv classcard-body储蓄率span classfw-bold${rate}%/span/div/div/div ); // 饼图 let pieChart echarts.init(document.getElementById(pie-chart)); pieChart.setOption({ tooltip:{trigger:item}, legend:{top:bottom}, series:[{type:pie, data:[{value:eleSumwatSum,name:水电费}, {value:entSum,name:娱乐消费}, {value:livSum,name:生活消费}] }] }); // 近6个月收支折线图 let months []; let now new Date(); for(let i5;i0;--i){let dnew Date(now.getFullYear(),now.getMonth()-i); months.push(d.getFullYear()-String(d.getMonth()1).padStart(2,0)); } let inArr months.map(m{ let item income.find(dd.datem); return item ? item.amount : 0; }); let spendArr months.map(m{ let util utilRecs.filter(dgetMonth(d.date)m); let ele0,wat0; for(let i1;iutil.length;i){eleutil[i].electric-util[i-1].electric;watutil[i].water-util[i-1].water;} ele*elePrice; wat*watPrice; let entM ent.filter(dgetMonth(d.date)m).reduce((a,b)ab.amount,0); let livM liv.filter(dgetMonth(d.date)m).reduce((a,b)ab.amount,0); return elewatentMlivM; }); echarts.init(document.getElementById(line-chart)).setOption({ tooltip:{trigger:axis}, legend:{data:[收入,支出]}, xAxis:{type:category,data:months},yAxis:{type:value}, series:[{name:收入,type:line,data:inArr},{name:支出,type:line,data:spendArr}] }); // 近6个月各类目支出柱状图 echarts.init(document.getElementById(bar-chart)).setOption({ tooltip:{trigger:axis},legend:{data:[水电费,娱乐消费,生活消费]}, xAxis:{type:category,data:months},yAxis:{type:value}, series:[ {name:水电费,type:bar,data:months.map(m{ let utilutilRecs.filter(dgetMonth(d.date)m); let ele0,wat0; for(let i1;iutil.length;i){ eleutil[i].electric-util[i-1].electric; watutil[i].water-util[i-1].water; } ele*elePrice; wat*watPrice; return (elewat).toFixed(2); })}, {name:娱乐消费,type:bar,data:months.map(ment.filter(dgetMonth(d.date)m).reduce((a,b)ab.amount,0))}, {name:生活消费,type:bar,data:months.map(mliv.filter(dgetMonth(d.date)m).reduce((a,b)ab.amount,0))}, ] }); } 4.utility.js function renderUtilityChart(records, elePrice, watPrice) { let dates [], eleUsage [], watUsage [], eleFee [], watFee []; for(let i0;irecords.length;i){ dates.push(records[i].date); let prev i0 ? records[i-1] : null; let eu prev ? (records[i].electric-prev.electric) : 0; let wu prev ? (records[i].water-prev.water) : 0; eleUsage.push(parseFloat(eu.toFixed(2))); watUsage.push(parseFloat(wu.toFixed(2))); eleFee.push(parseFloat((eu*elePrice).toFixed(2))); watFee.push(parseFloat((wu*watPrice).toFixed(2))); } let chart echarts.init(document.getElementById(utility-chart)); chart.setOption({ tooltip: { trigger:axis }, legend: { data:[用电量,用水量,电费,水费] }, xAxis: { type:category, data: dates }, yAxis: { type:value }, series: [ {name:用电量, data:eleUsage, type:line}, {name:用水量, data:watUsage, type:line}, {name:电费, data:eleFee, type:line}, {name:水费, data:watFee, type:line}, ] }); }templates文件夹1.base.html !DOCTYPE html html langzh head meta charsetUTF-8 title费用 Tracker/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.2/dist/css/bootstrap.min.css relstylesheet link relstylesheet href{{ url_for(static, filenamecss/style.css) }} script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script srchttps://code.jquery.com/jquery-3.7.1.min.js/script /head body stylebackground:#f8fafc; nav classnavbar navbar-expand-lg navbar-light bg-light mb-4 div classcontainer a classnavbar-brand href{{ url_for(utility) }}费用 Tracker/a ul classnavbar-nav me-auto mb-2 mb-lg-0 li classnav-itema classnav-link href{{ url_for(utility) }}水电费/a/li li classnav-itema classnav-link href{{ url_for(entertainment) }}娱乐消费/a/li li classnav-itema classnav-link href{{ url_for(living) }}生活消费/a/li li classnav-itema classnav-link href{{ url_for(overview) }}总览可视化/a/li /ul /div /nav div classcontainer {% block content %}{% endblock %} /div /body /html 2.entertainment.html {% extends base.html %} {% block content %} h2娱乐消费/h2 form methodpost classmb-3 div classrow mb-2 div classcol label日期/label input typedate namedate classform-control required /div div classcol label金额 (元)/label input typenumber step0.01 nameamount classform-control required /div div classcol label备注/label input typetext nameremark classform-control /div div classcol d-flex align-items-end button classbtn btn-primary w-100保存/button /div /div /form div identertainment-chart styleheight:380px;/div script src{{ url_for(static, filenamejs/entertainment.js) }}/script script renderEntertainmentChart({{ data.records|tojson }}); /script table classtable table-bordered mt-3 thead stylebackground:#e2e6ea; tr th日期/th th金额/th th备注/th th操作/th /tr /thead tbody {% for rec in data.records %} tr td{{ rec.date }}/td td{{ rec.amount }}/td td{{ rec.remark }}/td tda classbtn btn-sm btn-danger href{{ url_for(entertainment_delete, daterec.date) }}删除/a/td /tr {% endfor %} /tbody /table {% endblock %} 3.living.html {% extends base.html %} {% block content %} h2生活消费/h2 form methodpost classmb-3 div classrow mb-2 div classcol label日期/label input typedate namedate classform-control required /div div classcol label金额 (元)/label input typenumber step0.01 nameamount classform-control required /div div classcol label备注/label input typetext nameremark classform-control /div div classcol d-flex align-items-end button classbtn btn-primary w-100保存/button /div /div /form div idliving-chart styleheight:380px;/div script src{{ url_for(static, filenamejs/living.js) }}/script script renderLivingChart({{ data.records|tojson }}); /script table classtable table-bordered mt-3 thead stylebackground:#e2e6ea; tr th日期/th th金额/th th备注/th th操作/th /tr /thead tbody {% for rec in data.records %} tr td{{ rec.date }}/td td{{ rec.amount }}/td td{{ rec.remark }}/td tda classbtn btn-sm btn-danger href{{ url_for(living_delete, daterec.date) }}删除/a/td /tr {% endfor %} /tbody /table {% endblock %} 4.overview.html {% extends base.html %} {% block content %} h2总览可视化/h2 form methodpost classmb-3 div classrow div classcol-md-6 label月份 (YYYY-MM)/label input typemonth namemonth classform-control required /div div classcol-md-4 label收入 (元)/label input typenumber step0.01 nameincome classform-control required /div div classcol-md-2 d-flex align-items-end button classbtn btn-primary w-100保存/button /div /div /form div idoverview-cards classrow mb-3/div div idpie-chart styleheight:260px; classmb-3/div div idline-chart styleheight:260px; classmb-3/div div idbar-chart styleheight:260px;/div script src{{ url_for(static, filenamejs/overview.js) }}/script script renderOverviewCharts( {{ utility.records|tojson }}, {{ utility.unit_price.electric }}, {{ utility.unit_price.water }}, {{ entertainment.records|tojson }}, {{ living.records|tojson }}, {{ income.records|tojson }} ); /script {% endblock %} 5.utility.html {% extends base.html %} {% block content %} h2水电费/h2 {# 校验错误提示仅在新增日期不合法时显示保持 Bootstrap 风格 #} {% if error_msg %} div classalert alert-danger rolealert ⚠️ {{ error_msg }} /div {% endif %} form methodpost classmb-3 div classrow mb-2 div classcol label日期/label input typedate namedate classform-control required /div div classcol label电表读数/label input typenumber step0.01 nameelectric classform-control required /div div classcol label水表读数/label input typenumber step0.01 namewater classform-control required /div div classcol d-flex align-items-end button classbtn btn-primary w-100保存记录/button /div /div /form form methodpost classmb-3 div classrow mb-2 div classcol label电费单价 (元/度)/label input typenumber step0.01 nameele_unit value{{ data.unit_price.electric }} classform-control required /div div classcol label水费单价 (元/吨)/label input typenumber step0.01 namewat_unit value{{ data.unit_price.water }} classform-control required /div div classcol d-flex align-items-end button classbtn btn-info w-100设置单价/button /div /div /form div idutility-chart styleheight:380px;/div script src{{ url_for(static, filenamejs/utility.js) }}/script script renderUtilityChart({{ data.records|tojson }}, {{ data.unit_price.electric }}, {{ data.unit_price.water }}); /script table classtable table-bordered mt-3 thead stylebackground:#e2e6ea; tr th日期/th th电表读数/th th水表读数/th th用电量/th th用水量/th th电费/th th水费/th th操作/th /tr /thead tbody {% for rec in data.records %} tr td{{ rec.date }}/td td{{ rec.electric }}/td td{{ rec.water }}/td td{{ loop.index0 0 and (rec.electric - data.records[loop.index0-1].electric)|round(2) or 0 }}/td td{{ loop.index0 0 and (rec.water - data.records[loop.index0-1].water)|round(2) or 0 }}/td td{{ loop.index0 0 and ((rec.electric - data.records[loop.index0-1].electric)*data.unit_price.electric)|round(2) or 0 }}/td td{{ loop.index0 0 and ((rec.water - data.records[loop.index0-1].water)*data.unit_price.water)|round(2) or 0 }}/td tda classbtn btn-sm btn-danger href{{ url_for(utility_delete, daterec.date) }}删除/a/td /tr {% endfor %} /tbody /table {% endblock %}app.py# -*- coding: utf-8 -*- # 费用 Tracker Flask 主入口详细注释 from flask import Flask, render_template, request, redirect, url_for import os from services.storage import load_json, save_json app Flask(__name__) DATA_DIR os.path.join(os.path.dirname(__file__), data) def get_util_json(): return load_json(os.path.join(DATA_DIR, utility.json)) def get_ent_json(): return load_json(os.path.join(DATA_DIR, entertainment.json)) def get_liv_json(): return load_json(os.path.join(DATA_DIR, living.json)) def get_income_json(): return load_json(os.path.join(DATA_DIR, income.json)) def sort_records_by_date(data): records data.setdefault(records, []) if len(records) 1: records.sort(keylambda r: r[date]) return records def upsert_record(records, record): date record[date] for idx, rec in enumerate(records): if rec[date] date: records[idx] record return records.append(record) app.route(/) def index(): # 首页重定向到水电费 return redirect(url_for(utility)) app.route(/utility, methods[GET, POST]) def utility(): # 水电费页面新增/单价设置 file_path os.path.join(DATA_DIR, utility.json) data load_json(file_path) error_msg None # 校验错误提示仅水电费新增时使用 # 漏洞③修复进入路由先按日期排序避免历史数据顺序错乱导致相邻差值为负 records sort_records_by_date(data) if request.method POST: if date in request.form: # 新增/更新水电费记录 date request.form[date] electric float(request.form[electric]) water float(request.form[water]) record {date: date, electric: electric, water: water} # 性能优化一次遍历同时找同日期、上一条、下一条、最新日期 existing_idx None prev_rec None next_rec None latest_date records[-1][date] if records else None for idx, rec in enumerate(records): rec_date rec[date] if rec_date date: existing_idx idx continue if rec_date date: prev_rec rec continue if rec_date date: next_rec rec break is_update existing_idx is not None # 校验新记录日期必须 已有最新记录日期 # 如果提交的日期与已有记录中的某一条相同 - 视为覆盖更新放行日期检查 # 否则视为新增必须 现有记录中的最新日期 if not is_update and latest_date is not None and date latest_date: error_msg ( f新增记录的日期{date}必须大于或等于 f上一条最新记录日期{latest_date}。 ) # 校验失败不保存直接渲染当前页面并显示错误 return render_template(utility.html, datadata, error_msgerror_msg) # # 漏洞①②修复读数单调性校验覆盖更新分支同样要走 # 找到上一条日期严格小于当前日期的最后一条新读数不得小于它 if prev_rec is not None: if electric prev_rec[electric]: error_msg ( f电表读数{electric}不能小于上一条记录 f{prev_rec[date]} 的 {prev_rec[electric]}。 ) return render_template(utility.html, datadata, error_msgerror_msg) if water prev_rec[water]: error_msg ( f水表读数{water}不能小于上一条记录 f{prev_rec[date]} 的 {prev_rec[water]}。 ) return render_template(utility.html, datadata, error_msgerror_msg) # 找到下一条日期严格大于当前日期的第一条新读数不得大于它 # 这层主要针对覆盖更新场景把旧记录改小/改大都不能破坏单调性 if next_rec is not None: if electric next_rec[electric]: error_msg ( f电表读数{electric}不能大于下一条记录 f{next_rec[date]} 的 {next_rec[electric]}。 ) return render_template(utility.html, datadata, error_msgerror_msg) if water next_rec[water]: error_msg ( f水表读数{water}不能大于下一条记录 f{next_rec[date]} 的 {next_rec[water]}。 ) return render_template(utility.html, datadata, error_msgerror_msg) # # 校验通过覆盖同日期记录或追加新记录 if existing_idx is not None: records[existing_idx] record else: records.append(record) # 漏洞③修复写入前再排一次序保证存盘后顺序严格按日期 sort_records_by_date(data) save_json(file_path, data) elif ele_unit in request.form and wat_unit in request.form: # 设置单价 data[unit_price] { electric: float(request.form[ele_unit]), water: float(request.form[wat_unit]) } save_json(file_path, data) return redirect(url_for(utility)) return render_template(utility.html, datadata, error_msgerror_msg) app.route(/utility/delete/date) def utility_delete(date): file_path os.path.join(DATA_DIR, utility.json) data load_json(file_path) records data.get(records, []) data[records] [rec for rec in records if rec[date] ! date] # 漏洞③修复删除后同样按日期排序后再存盘 sort_records_by_date(data) save_json(file_path, data) return redirect(url_for(utility)) app.route(/entertainment, methods[GET, POST]) def entertainment(): file_path os.path.join(DATA_DIR, entertainment.json) data load_json(file_path) if request.method POST: date request.form[date] amount float(request.form[amount]) remark request.form.get(remark, ) record {date: date, amount: amount, remark: remark} records data.setdefault(records, []) upsert_record(records, record) save_json(file_path, data) return redirect(url_for(entertainment)) return render_template(entertainment.html, datadata) app.route(/entertainment/delete/date) def entertainment_delete(date): file_path os.path.join(DATA_DIR, entertainment.json) data load_json(file_path) records data.get(records, []) data[records] [rec for rec in records if rec[date] ! date] save_json(file_path, data) return redirect(url_for(entertainment)) app.route(/living, methods[GET, POST]) def living(): file_path os.path.join(DATA_DIR, living.json) data load_json(file_path) if request.method POST: date request.form[date] amount float(request.form[amount]) remark request.form.get(remark, ) record {date: date, amount: amount, remark: remark} records data.setdefault(records, []) upsert_record(records, record) save_json(file_path, data) return redirect(url_for(living)) return render_template(living.html, datadata) app.route(/living/delete/date) def living_delete(date): file_path os.path.join(DATA_DIR, living.json) data load_json(file_path) records data.get(records, []) data[records] [rec for rec in records if rec[date] ! date] save_json(file_path, data) return redirect(url_for(living)) app.route(/overview, methods[GET, POST]) def overview(): # 总览页面处理收入表单和数据展示 file_path os.path.join(DATA_DIR, income.json) income_data load_json(file_path) if request.method POST: month request.form[month] income float(request.form[income]) record {date: month, amount: income} records income_data.setdefault(records, []) upsert_record(records, record) save_json(file_path, income_data) return redirect(url_for(overview)) utility get_util_json() entertainment get_ent_json() living get_liv_json() return render_template(overview.html, utilityutility, entertainmententertainment, livingliving, incomeincome_data) if __name__ __main__: app.run(debugTrue)entertainment.json{ records: [ { date: 2026-06-23, amount: 3.0, note: } ] }README.md# 费用 Tracker 使用说明 1. 安装依赖 bash pip install -r requirements.txt 2. 启动应用 bash python app.py 3. 浏览器访问 http://127.0.0.1:5000 本地数据自动保存在 data 目录下无需数据库requirements.txtFlask3.0.3utility.json{ records: [ { date: 2026-06-22, electric: 12.0, water: 1.0 }, { date: 2026-06-23, electric: 15.0, water: 5.0 }, { date: 2026-06-24, electric: 20.0, water: 8.0 } ], unit_price: { electric: 0.68, water: 3.0 } }