资讯详情

微信小程序个人财务管理系统毕业设计:表结构、云开发与统计落地

📅 2026/9/17 19:37:55 | 华诺云谱 👁 阅读
微信小程序个人财务管理系统毕业设计:表结构、云开发与统计落地
简介这份资源是一篇面向计算机专业毕业设计场景的完整论文文档主题为基于微信小程序的个人财务管理系统适合正在选题、撰写论文或准备答辩的本科生参考借鉴。文档围绕收支信息管理这一核心业务展开涉及Uni-weixin小程序开发框架、SpringBoot后端架构、Java语言与MySQL数据库存储等关键技术并包含系统分析、开发技术选型、功能设计与数据安全等章节内容可作为同类移动端管理系统的写作模板。资源包共1个docx文件压缩包约2.92MB文件体积小巧、便于本地阅读与二次编辑目录结构完整含中英文摘要与关键词方便直接对照格式调整。目前已有448人学习下载读者可从中获取赛题式的选题思路、章节组织方式、技术方案描述与功能模块划分方法尤其适合需要快速理解小程序类毕设论文写作框架、查漏补缺的中初级开发者与应届毕业生参考使用。1. 微信小程序个人财务管理系统毕业设计怎么落地很多同学选这个题理由很实际记账软件用户不需要再装一个 App微信里点开就能记一笔答辩时演示也方便。但真正动手之后会发现难的从来不是记一笔这个动作而是围绕它的整条链路——登录怎么拿到用户身份、账单表怎么设计、分类统计怎么出图、真机上请求为什么发不出去、答老师问的时候能不能说清数据隔离在哪一层做的。个人财务管理系统在毕设里出现频率高恰恰因为它是麻雀虽小五脏俱全登录鉴权、增删改查、数据聚合、图表可视化、文件留存五件事一个不缺任何一件没打通系统就是半成品。而小程序这一侧又有自己的脾气request 合法域名、基础库版本、setData性能、云开发环境隔离坑都集中在这些看起来不起眼的配置上。接下来按表结构 → 后端选型 → 记账与统计实现 → 真机排错与答辩演示四段推进代码以微信小程序原生加云开发为主线同时给出自建后端的对照写法。适合正在做这类毕设的本科生也适合想快速复现一套记账小程序的开发者。数据只做本地记账不涉及任何理财建议或资金业务这一点在审核和答辩时都要主动说清楚。2. 个人财务管理系统的表结构与页面骨架先定数据模型再写页面。顺序反过来的话做到统计页一定会返工。个人财务管理系统说白了就四张表谁在记、记的是哪一类、记了多少钱、这个月准备花多少。2.1 个人财务管理系统的四张核心表怎么设计表名关键字段索引建议说明users_openid、nickName、avatarUrl、createdAt_openid唯一首次登录时写入后续只读categories_openid、name、icon、type、sort_openid type 复合type 取 expense / incomebills_openid、categoryId、type、amount、remark、billDate、createdAt_openid billDate 复合账单主表金额存分或保留两位小数budgets_openid、month、amount_openid month 唯一month 形如 2025-06金额字段建议直接用Number存元但写入前统一Math.round(value * 100) / 100避免浮点误差在统计页被放大成 0.30000000000000004。账单表里billDate单独存一条YYYY-MM-DD字符串比只存时间戳更好查——按月聚合时直接db.command.gte(2025-06-01).and(db.command.lt(2025-07-01))不用做时区换算。2.2 app.json 里注册记账页面与 tabBar 的最小配置{ pages: [ pages/index/index, pages/record/record, pages/stat/stat, pages/mine/mine ], window: { navigationBarTitleText: 个人记账, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black }, tabBar: { color: #999999, selectedColor: #07c160, list: [ { pagePath: pages/index/index, text: 明细 }, { pagePath: pages/record/record, text: 记一笔 }, { pagePath: pages/stat/stat, text: 统计 }, { pagePath: pages/mine/mine, text: 我的 } ] }, lazyCodeLoading: requiredComponents }lazyCodeLoading设成requiredComponents之后小程序只注入当前页面用到的自定义组件首屏启动会明显快一些尤其是统计页引了 echarts 这类体积较大的组件时。tabBar的iconPath可以暂时不配只显示文字也能跑通等 UI 定稿再补图。2.3 分类初始数据的批量写入与_openid的坑分类数据不能写死在前端否则统计页的餐饮/交通这类名字改一次要发一次版本。常见做法是首次进入时由云函数初始化一份。// cloudfunctions/initCategories/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() const DEFAULT [ { name: 餐饮, type: expense, sort: 1 }, { name: 交通, type: expense, sort: 2 }, { name: 购物, type: expense, sort: 3 }, { name: 工资, type: income, sort: 1 } ] exports.main async () { const { OPENID } cloud.getWXContext() // 幂等已经初始化过就跳过防止重复插入 const exist await db.collection(categories).where({ _openid: OPENID }).count() if (exist.total 0) return { inserted: 0 } await Promise.all(DEFAULT.map(item db.collection(categories).add({ data: { ...item, _openid: OPENID, // 云函数写库不会自动补 _openid必须手写 createdAt: db.serverDate() } }) )) return { inserted: DEFAULT.length } }这里最容易踩的一脚是在小程序端直接db.collection().add()时微信会自动补_openid但在云函数里写库不会。如果漏了这行用户第二次进来查分类查不到因为_openid是空的。函数开头的count()判断是为了幂等避免用户反复下拉刷新导致分类列表越刷越长。3. 后端选型云开发与自建后端怎么选登录鉴权怎么做3.1 云开发和自建后端个人财务管理系统的两种落地路线维度云开发自建后端Spring Boot / Node上线速度当天可跑通写库需部署 证书 域名鉴权方式cloud.getWXContext()直接取 openid自己走 code2Session 换 openid 再签 token域名与备案不需要HTTPS 域名需备案并配置合法域名答辩可控性演示依赖网络可本地起服务现场更稳能体现的能力前端 云函数完整后端分层、SQL、接口设计如果你的毕设重点是后端能力选自建如果重点是完整可跑的产品选云开发。两者不冲突也可以云开发打通后再补一层 Node 中间层把聚合统计挪到后端去算。3.2 wx.login 到 code2Session记账小程序的登录鉴权链路小程序端调wx.login拿到的是五分钟有效、一次性使用的code它换不出用户信息必须送到服务端或云函数里换openid。# 在服务端验证 appid/secret 是否配对js_code 只能消费一次 curl -G https://api.weixin.qq.com/sns/jscode2session \ --data-urlencode appidwx1234567890abcdef \ --data-urlencode secret服务端保存的AppSecret \ --data-urlencode js_code081Abc... \ --data-urlencode grant_typeauthorization_code返回体里的openid才是这个用户在你的小程序里的唯一标识session_key只用于解密手机号等加密数据绝不能下发到小程序端。前端调用长这样// pages/mine/mine.js wx.login({ success: async ({ code }) { const res await wx.cloud.callFunction({ name: login, data: { code } }) this.setData({ openid: res.result.openid }) } })云函数版的login里拿OPENID甚至不需要codecloud.getWXContext()会由微信侧注入伪造不了。自建后端则要把上一步curl的结果转成自己签发的 token后续每个记账接口都校验这个 token 对应的userId。3.3 云函数里写账单用 OPENID 做数据隔离数据隔离不是靠前端传个userId过滤而是靠服务端强制注入。// cloudfunctions/billAdd/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event) { const { OPENID } cloud.getWXContext() const { categoryId, type, amount, remark, billDate } event // 白名单校验别把前端传来的字段整包写进库 if (![expense, income].includes(type)) return { code: 400, msg: type 非法 } if (typeof amount ! number || amount 0) return { code: 400, msg: 金额非法 } const { _id } await db.collection(bills).add({ data: { _openid: OPENID, categoryId, type, amount: Math.round(amount * 100) / 100, remark: String(remark || ).slice(0, 50), billDate, createdAt: db.serverDate() } }) return { code: 0, id: _id } }金额和类型必须校验因为小程序端可以被改包重放。remark截断 50 字是防止用户贴进一大段文本把单条记录撑爆。所有查询接口一律以cloud.getWXContext().OPENID为条件绝不用前端传的_openid这就是答辩时最能讲清楚的一句话。4. 记账表单、分类统计与图表渲染的落地写法4.1 记账页表单金额输入、单选框分类和日期选择view classform radio-group classtype-group bindchangeonTypeChange labelradio valueexpense checked{{type expense}} /支出/label labelradio valueincome checked{{type income}} /收入/label /radio-group input classamount typedigit value{{amount}} placeholder0.00 bindinputonAmountInput / picker range{{categoryNames}} value{{categoryIndex}} bindchangeonCategoryChange view classpicker分类{{categoryNames[categoryIndex]}}/view /picker picker modedate value{{billDate}} bindchangeonDateChange view classpicker日期{{billDate}}/view /picker button bindtapsubmit保存/button /viewinput的type必须写digit写number调出来的键盘没有小数点用户输入 12.5 会变成 125。radio-group的bindchange事件里e.detail.value直接就是被选中项的value但checked属性只认字符串比较所以 JS 里type要初始化为expense而不是布尔值。Page({ data: { type: expense, amount: , billDate: , categories: [], categoryNames: [], categoryIndex: 0 }, onLoad() { this.loadCategories() }, async loadCategories() { const { result } await wx.cloud.callFunction({ name: categoryList, data: { type: this.data.type } }) this.setData({ categories: result.list, categoryNames: result.list.map(i i.name), categoryIndex: 0 }) }, onAmountInput(e) { this.setData({ amount: e.detail.value }) }, onTypeChange(e) { this.setData({ type: e.detail.value }, () this.loadCategories()) } })切换收支类型后分类列表必须重载否则会出现收入下面挂着餐饮这种脏数据。setData的第二个参数是回调在视图更新完才执行正好用来做这种级联加载。4.2 用 ec-canvas 画月度支出折线图与分类饼图小程序的 canvas 和浏览器不完全一样直接用 echarts 官网的 CDN 包跑不起来常见做法是引入ec-canvas组件把 echarts 打包进去。// pages/stat/stat.js import * as echarts from ../../components/ec-canvas/echarts function initLine(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width, height, devicePixelRatio: dpr }) canvas.setChart(chart) chart.setOption({ grid: { left: 40, right: 16, top: 24, bottom: 28 }, tooltip: { trigger: axis }, xAxis: { type: category, data: [] }, yAxis: { type: value }, series: [{ type: line, smooth: true, data: [] }] }) return chart } Page({ data: { ecLine: { onInit: initLine } }, onReady() { this.render() }, async render() { const { result } await wx.cloud.callFunction({ name: statDaily }) // 拿到实例后只更新数据不要重复 init const chart this.selectComponent(#line).chart chart.setOption({ xAxis: { data: result.days }, series: [{ data: result.totals }] }) } })关键是devicePixelRatio: dpr这一项漏掉图表在高分屏上会糊成一片。另外onInit只会在组件首次渲染时调一次后续刷新数据要用this.selectComponent().chart拿实例再setOption重复 init 会导致图表叠加、内存上涨。折线图适合看这个月哪天花得多饼图适合看钱花在哪一类两个图共用同一份聚合结果只是分组维度不同。4.3 统计聚合查询云数据库 aggregate 与 MySQL 写法对照前端把整月账单拉回来再算数据量一上来就卡。正确做法是让数据库算// cloudfunctions/statByCategory/index.js const db cloud.database() const $ db.command.aggregate const res await db.collection(bills).aggregate() .match({ _openid: OPENID, type: expense, billDate: db.command.gte(2025-06-01).and(db.command.lt(2025-07-01)) }) .group({ _id: $categoryId, total: $.sum($amount), count: $.sum(1) }) .sort({ total: -1 }) .end()对应的 MySQL 写法是同一套逻辑SELECT category_id, SUM(amount) AS total, COUNT(*) AS cnt FROM t_bill WHERE user_id ? AND type expense AND bill_date 2025-06-01 AND bill_date 2025-07-01 GROUP BY category_id ORDER BY total DESC;注意match必须放在group前面聚合管道是从上往下执行的顺序反了会把全表拉进内存。金额用$.sum($amount)而不是$.sum(1)后者的含义是计数。如果分类名要显示出来可以在聚合后拿_id列表再查一次categories做一次内存 join比在云数据库里做lookup更省事。5. 真机调试、基础库与答辩演示的收尾技巧5.1 真机调试请求打不通的四步定位开发者工具里一切正常、真机上转圈圈是这类项目最高频的问题。按顺序查开发者工具的详情 → 本地设置 → 不校验合法域名在真机上不生效自建后端必须把域名填进小程序后台的 request 合法域名且必须是 HTTPS。证书链是否完整。部分安卓机型对中间证书缺失很敏感用openssl s_client -connect 你的域名:443 -servername 你的域名看返回里有没有完整的证书链。服务端是否只监听了内网地址或者安全组没放行 443。打开真机调试的 vConsole直接看 Network 里那条 request 的状态码是超时、403 还是证书错误一看就分得清。云开发路线基本不会遇到这些这也是答辩现场我更推荐云开发演示的原因——不依赖你宿舍的网络。5.2 自定义导航栏高度与基础库版本设置想要沉浸式顶部时导航栏高度不能写死 44pxconst win wx.getWindowInfo() // 状态栏高度 const menu wx.getMenuButtonBoundingClientRect() // 胶囊按钮位置 const navHeight (menu.top - win.statusBarHeight) * 2 menu.height this.setData({ statusBarHeight: win.statusBarHeight, navHeight })wx.getSystemInfoSync已经不再推荐使用新项目统一换成wx.getWindowInfo。基础库版本在开发者工具详情 → 本地设置 → 调试基础库里切换后端云函数能力、wx.getWindowInfo这些 API 都有最低版本要求答辩机器上最好固定成一个稳定版本再录屏备份。5.3 setData 路径写法与附件本地存储 wx.env.USER_DATA_PATH账单详情页只想改昵称时别整个对象重设// 只更新一个字段避免把整个 userInfo 序列化过桥 this.setData({ userInfo.nickName: 张三 })带路径的写法能让setData的传输量降到最小列表页滚动时尤其明显。用户拍发票留档可以先用wx.env.USER_DATA_PATH写到本地临时目录const fs wx.getFileSystemManager() const filePath ${wx.env.USER_DATA_PATH}/invoice_${Date.now()}.jpg fs.writeFileSync(filePath, base64Data, base64)wx.env.USER_DATA_PATH是小程序自己的可写目录卸载即清空适合做草稿缓存要长期留档还是得wx.cloud.uploadFile传云存储再把fileID存进账单表本地路径换个手机就找不到了。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。