简介:基于微信小程序与 Java 后端构建的家庭理财管理系统毕业设计资源,面向计算机专业学生和初学小程序开发的读者。系统以家庭为单位管理日常收支,实现了用户注册登录、后台管理、工资管理、记账本、贷款管理、理财分析等模块,并针对家庭收支数据进行自动汇总与可视化分析;管理员可删除普通用户、修改账号密码和用户信息,贷款管理则支持设置贷款金额、日期、利率与还款提醒,整体界面采用蓝色主调,理财分析和贷款模块用绿色强化区分。压缩包共 4 个文件、约 4.62MB,其中两个 ZIP 分别对应前端小程序工程和后端服务,另含 SQL 数据库脚本与 TXT 说明文档,便于对照还原运行环境。目前已有 314 人学习下载。配套毕业论文从需求分析、业务流程到界面设计和理财分析功能实现思路均给出了较完整阐述,适合作为课题设计、答辩展示或二次开发的基础参照。
1. 这个毕设题到底在做什么:先用微信小程序把“管钱”这件事跑通
拿到“基于微信小程序的家庭理财管理系统设计与实现+毕业论文”这个标题,第一反应是:这不就是一个带界面的记账本吗?但真正动手做过的人都会意识到,这个题目把小程序开发、后端接口、数据库设计、用户登录体系、图表可视化,外加一篇能过审的毕业论文全部压在了同一个项目上。它的本质不是“记账”,而是把一个家庭的钱怎么进、怎么出、怎么控制预算这件事,完整地搬到微信里。用微信做载体是因为用户不需要下载 App,打开即用,天然适合家庭成员之间共享一个账本。这个题适合三类人:正在选毕设方向的学生、想快速练手小程序前后端全流程的开发者、以及想给家里做一个能实际用的管钱工具的普通用户。整个项目可以拆成五条线推进:技术选型、数据建模、后端接口、小程序页面、论文写作,下面逐条展开。
2. 技术选型与整体架构:原生小程序加自建后端是最稳妥的组合
2.1 原生小程序框架和第三方跨端框架怎么选:毕设场景别折腾编译器
很多人在第一步就会纠结:用微信小程序原生框架,还是用 Taro、uni-app 这类跨端框架。我的建议很直接:如果是毕设,就用原生。原因有三个。第一,原生框架的调试链路最短,微信开发者工具打开即用,wxml 报错能直接定位到具体行,跨端框架一旦出现编译层报错,你分不清是自己代码的问题还是编译器的问题。第二,原生框架生成的代码量更可控,论文里贴代码片段时,评审老师能直接看懂,不用先解释这一层是 Vue 语法还是 React 语法。第三,跨端框架目前对自定义组件、分包加载的支持仍然存在边界情况,家庭理财系统里要做图表统计,越接近底层越不容易被框架限制。
原生小程序的项目结构基本是固定的,入口文件 app.json 负责全局配置,页面放在 pages 目录下,每个页面由 wxml、wxss、js、json 四个文件组成。以这个项目为例,至少需要五个页面:首页展示本月收支概况、记账页负责录入账单、统计页展示支出分类占比、家庭页管理家庭成员、我的页面负责登录和个人设置。页面注册在 app.json 里写清楚即可:
{ "pages": [ "pages/index/index", "pages/record/record", "pages/stats/stats", "pages/family/family", "pages/mine/mine" ], "window": { "navigationBarTitleText": "家庭理财", "navigationBarBackgroundColor": "#2d8cf0", "navigationBarTextStyle": "white" }, "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/record/record", "text": "记账" }, { "pagePath": "pages/stats/stats", "text": "统计" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }这段配置里,pages 数组的第一项就是小程序的启动页,一般把首页放最前面。tabBar 是底部的导航栏,注意 tabBar 的页面路径必须和 pages 数组里的一致,否则编译会直接报错。navigationBarBackgroundColor 可以换成你家庭理财主题的主色调,但深色背景配白色标题是通用习惯,浅色背景记得把 navigationBarTextStyle 改成 black。这里有个隐藏细节:tabBar 最多配置 5 个页面,当前 4 个完全够用,记账页作为最高频操作建议放在中间,符合拇指操作习惯。
2.2 后端选型:Node.js 加 MySQL 还是微信云开发,论文的可写性差很多
小程序本身不能直接操作数据库,必须经过后端接口中转。这里有两种主流选型:微信云开发,或者自建服务端加数据库。云开发的优势是免运维,登录、数据库、存储都是现成的,但劣势也很明显——代码量少了一半,答辩时老师一句“这些是平台帮你做的还是你自己写的”就能问住你。我更倾向的是一套更“传统”的方案:自建 Node.js 后端,用 Express 框架提供接口,MySQL 存储数据。整套链路跑下来,论文里的系统架构图、时序图、数据库设计都有东西可写,工作量也足够填充毕业设计的要求。
以 Node.js 为例,初始化一个项目只需要三步:安装依赖、创建入口文件、启动服务。用到的核心依赖只有三个:express 负责路由,mysql2 负责连接数据库,jsonwebtoken 负责生成登录令牌。在项目目录下执行初始化命令后,写一个最小的服务入口:
const express = require('express'); const mysql = require('mysql2/promise'); // 创建数据库连接池,避免频繁建立连接 const pool = mysql.createPool({ host: '127.0.0.1', // 本机开发环境 user: 'root', password: 'yourpassword', database: 'family_finance', waitForConnections: true, connectionLimit: 10, namedPlaceholders: true }); const app = express(); app.use(express.json()); // 解析小程序端传来的 JSON 请求体 app.get('/api/health', async (req, res) => { const [rows] = await pool.query('SELECT 1'); res.json({ status: 'ok', db: rows.length > 0 }); }); app.listen(3000, () => { console.log('服务已启动,端口 3000'); });这段代码里有三个关键点。第一,必须用连接池 mysql.createPool,而不是每次请求都创建新连接,因为家庭理财系统即使只是几个家庭成员同时用,也可能因为并发请求把数据库连接数打满,连接池的作用是复用连接而不是反复创建和销毁。第二,express.json() 是必选项,小程序端通过 wx.request 发送的是 JSON 格式的请求体,不加这个中间件,req.body 永远是 undefined,这是新手最容易遇到的问题。第三,MySQL 配置里 host 写 127.0.0.1 表示只监听本机,上线部署时改成云服务器的内网地址即可。这里故意没写密码进代码,实际项目应该通过环境变量读取,否则代码一提交,数据库密码就泄露了。启动后可以先访问一下 /api/health 接口,响应里能看到 db 字段为 true,说明数据库已经打通,后端骨架就站起来了。
2.3 数据流向与项目目录:一次记账请求经过的六个环节
从你在小程序里点“保存账单”,到屏幕上显示“保存成功”,数据经过的路径是固定的。小程序页面的 submit 函数先把表单数据组装成对象,调用 wx.request 发起网络请求,请求到达 Express 的路由层,路由根据 URL 匹配到对应的控制器方法,控制器方法从请求体里取出参数,拼装 SQL 语句交给 MySQL 查询,拿到结果后再按约定的格式返回给小程序端。任何一环出了问题,现象都表现为前端报错,但排查方向完全不同,这也是我建议你在动手前先画清楚这条链路的原因。
对应到项目目录,我习惯这样组织:
family-finance-server/ ├── app.js # 服务入口,注册路由 ├── config/ │ └── db.js # 数据库配置 ├── routes/ │ ├── auth.js # 登录、注册相关路由 │ └── bill.js # 账单增删改查路由 ├── controllers/ │ ├── authController.js │ └── billController.js └── utils/ └── response.js # 统一响应格式前端部分的目录就是小程序默认的 pages 结构,按页面划分而不是按功能划分,因为小程序原生框架本身就是一页一包的思路。这样的目录结构有两个好处:第一,前后端代码分离,论文里画系统架构图时可以理直气壮地写“本系统采用前后端分离架构”;第二,每个文件的职责单一,出问题时通过错误堆栈能快速定位到具体模块,不至于所有代码都堆在一个文件里翻来翻去。响应格式我一般统一成 { code: 0, data: ..., message: 'ok' },code 为 0 表示成功,非 0 表示业务错误,前端只需要判断 code 就能决定是弹错误提示还是正常渲染数据,不用每个接口单独写一套判断逻辑。
3. 数据库与后端接口落地:把家庭账本拆成可持续扩展的数据模型
3.1 核心数据表设计:六张表把家庭账本完整拆开
数据库设计是这个项目里最值得花时间的一步,因为表结构一旦定下来,后面所有接口都在为这些表服务。家庭理财系统最少需要六张表:用户表、家庭表、家庭成员关系表、账单表、分类表、预算表。很多新手会直接做一张用户表加一张账单表,用户表里塞一个 family_id 字段,看起来省事,但家庭成员可能会换家庭、一个用户可能同时属于多个家庭,关系表会让数据更干净。账单表是核心,字段需要覆盖金额、类型、分类、备注、记账人、关联家庭和时间。
下面这张表是一个最小可用的账单表结构,字段类型和含义都值得逐行看:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | 主键自增 | 账单唯一标识 |
| family_id | BIGINT | 索引,非空 | 关联家庭表,用于按家庭查询 |
| user_id | BIGINT | 索引,非空 | 记账人,关联用户表 |
| type | TINYINT | 非空 | 1表示支出,2表示收入 |
| category_id | BIGINT | 非空 | 关联分类表 |
| amount | DECIMAL(10,2) | 非空 | 金额,单位为元 |
| bill_time | DATETIME | 索引,非空 | 记账时间,注意不是创建时间 |
| remark | VARCHAR(200) | 默认空串 | 备注,允许为空 |
| created_at | DATETIME | 默认当前时间 | 记录创建时间 |
amount 字段用 DECIMAL(10,2),这是一个血泪教训。如果图省事用 FLOAT 或者 DOUBLE,金额会出现 0.1 加 0.2 不等于 0.3 的浮点误差,对账的时候会差出几分钱,虽然不多,但家庭账本的信任感就没了。bill_time 和 created_at 必须分开,家庭理财里有大量补记账的场景,用户可能今天补记昨天的账单,bill_time 代表这笔钱实际发生的日期,created_at 代表这笔数据录入系统的日期,如果不分开,统计月底报表时所有补记账都会被算错天。
对应的建表 SQL 可以这样写,注意索引的使用:
CREATE TABLE bill ( id BIGINT AUTO_INCREMENT PRIMARY KEY, family_id BIGINT NOT NULL, user_id BIGINT NOT NULL, type TINYINT NOT NULL COMMENT '1-支出 2-收入', category_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, bill_time DATETIME NOT NULL, remark VARCHAR(200) DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_family_time (family_id, bill_time), INDEX idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;两个索引是有讲究的。idx_family_time 用 family_id 和 bill_time 组成联合索引,因为最常跑的查询是“查这个家庭某个月的账单”,这个场景在 WHERE 条件里同时用到了 family_id 和 bill_time,联合索引可以一次命中。单列索引 idx_user 供“查这个用户最近记了哪些账”使用。第二,表引擎必须用 InnoDB,因为它支持事务,删除一条账单后如果后续操作失败,还能回滚,MyISAM 虽然查询快但不支持事务。字符集选 utf8mb4 是为了支持表情符号,微信聊天框里复制过来的备注很可能带 emoji,utf8mb3 会直接存不进去。分类表单独建一张,预设日常餐饮、交通、购物、住房、医疗等十多个固定分类,用户也可以自己加,比直接在账单表里存分类名称更规范,后面统计数据时按分类分组也更快。
3.2 后端登录与账单接口:关键代码和参数这样设最稳
用户体系是整个系统的基石。微信小程序登录和传统用户名密码登录最大的区别是,小程序端拿不到用户密码,只能用 wx.login 获取一个临时 code,然后交给后端调用微信接口换取 openid。openid 是用户在当前小程序下的唯一标识,永远不会变,用它作为用户的业务主键。下面给出登录接口的核心实现:
// controllers/authController.js const axios = require('axios'); exports.login = async (req, res) => { const { code, nickname, avatarUrl } = req.body; if (!code) { return res.json({ code: 1, message: '缺少 code 参数' }); } // 用 code 向微信服务端换 openid const wxResp = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { params: { appid: '你的AppID', secret: '你的AppSecret', js_code: code, grant_type: 'authorization_code' } }); const { openid } = wxResp.data; if (!openid) { return res.json({ code: 1, message: 'code 已过期,请重试' }); } // 查用户表,不存在则自动注册 let [users] = await pool.query('SELECT id, nickname FROM user WHERE openid = ?', [openid]); if (users.length === 0) { const [result] = await pool.query( 'INSERT INTO user (openid, nickname, avatar_url) VALUES (?, ?, ?)', [openid, nickname || '微信用户', avatarUrl || ''] ); users = [{ id: result.insertId, nickname: nickname || '微信用户' }]; } // 生成登录令牌,有效期 7 天 const token = jwt.sign({ userId: users[0].id }, process.env.JWT_SECRET, { expiresIn: '7d' }); res.json({ code: 0, data: { token, user: users[0] } }); };这里有两个必须注意的地方。第一,code 只能使用一次,且有效期只有五分钟,所以这个接口不允许前端把 code 缓存起来重复调用,否则会报 invalid code。第二,AppID 和 AppSecret 不能写在小程序前端代码里,因为小程序打包后的代码是公开的,别人可以从中提取出你的密钥,必须放在后端环境变量里。用 jwt 生成 token 的目的是后续每个接口都需要校验用户身份,token 里只存 userId,不存 openid 本身,避免敏感信息被解码看到。实际项目里还要加一层缓存,把 openid 和 userId 的映射放到 Redis 里,减少对微信接口的依赖,毕设阶段直接查数据库就够用了。
账单接口的写入逻辑,核心是参数校验和金额处理:
// controllers/billController.js exports.addBill = async (req, res) => { const { familyId, type, categoryId, amountStr, billTime, remark } = req.body; const userId = req.userId; // 来自登录中间件 // 金额校验:必须大于0,最多两位小数 const amount = Number(amountStr); if (!amount || amount <= 0 || !/^\d+(\.\d{1,2})?$/.test(amountStr)) { return res.json({ code: 1, message: '金额格式不正确' }); } // 校验家庭成员身份 const [members] = await pool.query( 'SELECT id FROM family_member WHERE user_id = ? AND family_id = ?', [userId, familyId] ); if (members.length === 0) { return res.json({ code: 1, message: '无权在此家庭记账' }); } const [result] = await pool.query( `INSERT INTO bill (family_id, user_id, type, category_id, amount, bill_time, remark) VALUES (?, ?, ?, ?, ?, ?, ?)`, [familyId, userId, type, categoryId, amount, billTime, remark] ); res.json({ code: 0, data: { id: result.insertId } }); };这段代码的价值全在校验上。金额校验用正则 \d+(.\d{1,2})? 限制最多两位小数,防止用户输入 0.001 这种在显示层出现三位小数的情况。家庭成员身份校验是很多人容易漏掉的:如果不检查,任何用户只要拿到 familyId 就能往别人家的账本里写数据,这在家庭理财系统里是严重的安全漏洞。中间件从 token 里解析出 userId 后挂载到 req 对象上,controller 里直接使用。这里 type 和 categoryId 的合法性没有在 controller 层校验,因为在前端记账页已经做了下拉框限制,后端再加一层分类表联动校验当然更稳,但会增加一次查询,毕设层面做到这一步已经够用。
3.3 统计接口:按分类和按月份聚合的 SQL 这样写
统计页是家庭理财的亮点功能,核心是用一条 SQL 完成按月、按分类的聚合。小程序的 canvas 图表负责画图,后端接口负责把数据查出来按固定格式返回。月度支出统计的典型查询是“当前月份,这个家庭一共花了多少钱,按分类分组”:
SELECT c.name AS category_name, SUM(b.amount) AS total_amount FROM bill b LEFT JOIN category c ON b.category_id = c.id WHERE b.family_id = ? AND b.type = 1 AND b.bill_time >= CONCAT(?, '-01') AND b.bill_time < DATE_ADD(CONCAT(?, '-01'), INTERVAL 1 MONTH) GROUP BY b.category_id ORDER BY total_amount DESC;回调参数的第一个问号传 family_id,第二个问号传如 '2025-04' 这样的月份字符串,CONCAT(?, '-01') 把它拼成当月第一天。日期范围查询的关键是结尾的时间用 < 下月第一天,而不是 <= 当月最后一天,这样可以避免“2025-04-30 23:59:59”和“2025-05-01 00:00:00”之间的边界漏洞。LEFT JOIN 保证即使分类表中找不到对应分类,账单也不会被丢弃,只是分类名称会是 NULL。ORDER BY total_amount DESC 让饼图的前几项正好是钱花最多的地方,一目了然。
4. 小程序端核心页面实现:从“能记账”到“看得出钱花哪了”
4.1 登录流程:openid 不是用户 ID,token 才需要存本地
小程序的登录流程和网页端有区别。网页端通常是输入用户名密码,小程序端则是静默登录:用户打开小程序,什么都不用做,前端调用 wx.login 拿到一个临时的 code,传给后端,后端去微信接口换成 openid,然后注册或查找用户,返回一个自定义的 token。这个 token 是后端自己发的,签名密钥只有后端知道,前端后续每个接口都在请求头里带上它。下面的代码展示前端如何发起登录:
// pages/mine/mine.js const app = getApp(); Page({ async onLoad() { const token = wx.getStorageSync('token'); if (!token) { await this.login(); } // token 存在则尝试拉取用户信息,失败说明 token 过期 }, async login() { const { code } = await wx.login(); const res = await request({ url: '/api/auth/login', method: 'POST', data: { code } }); if (res.code === 0) { wx.setStorageSync('token', res.data.token); wx.setStorageSync('userId', res.data.user.id); } } });这里的关键点在于,wx.login 拿到的 code 只能换一次 openid,而且 openid 属于敏感信息,不能直接返回给前端存储。后端返回的 token 才是应该持久化的凭证。token 放在小程序的 Storage 里,每次请求时取出放到 Header 的 Authorization 字段。这里要注意,wx.getStorageSync 是同步的,登录时页面可能有多个组件同时发起请求,容易出现 token 还没存好就去请求数据的情况。我一般会在 app.js 里维护一个全局的 pendingPromise,在登录进行时,后续所有请求都 await 这个 promise 完成再发出,避免并发登录。
4.2 记账页设计:金额、分类、时间,三个字段撑起整个页面的核心布局
记账页是整个小程序里用户停留时间最长的页面,所以它的交互设计比代码更重要。需要收集的数据是四个:收支类型(支出还是收入)、金额、分类、时间,加上一个可选项备注。其中金额是数字键盘输入,分类是弹出式面板选择,时间是默认当前的日期选择器。
下面是记账页提交函数的完整实现:
// pages/record/record.js Page({ data: { type: 1, // 1 支出,2 收入 amount: '', categoryId: '', billTime: '', categories: [] // 当前类型下的分类列表 }, onLoad() { this.setData({ billTime: this.formatToday() }); this.loadCategories(); }, formatToday() { const d = new Date(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${d.getFullYear()}-${m}-${day}`; }, async submitBill() { const { type, amount, categoryId, billTime, remark } = this.data; // 前端校验 + 提示,减少无效请求 if (!amount || Number(amount) <= 0) { wx.showToast({ title: '请输入正确金额', icon: 'none' }); return; } if (!categoryId) { wx.showToast({ title: '请选择分类', icon: 'none' }); return; } const res = await request({ url: '/api/bill/add', method: 'POST', data: { type, amount, categoryId, billTime: billTime + ' 12:00:00', remark } }); if (res.code === 0) { wx.showToast({ title: '记账成功', icon: 'success' }); this.setData({ amount: '', categoryId: '', remark: '' }); } } });这段代码里有几个操作层面的细节:billTime 传给后端时拼了一个固定的 ‘12:00:00’,这是因为日期选择器返回的只是日期字符串,而后端 bill_time 字段是 DATETIME,不拼时间会出错。固定用中午时间还可以避免跨时区导致的日期偏移。分类列表需要根据 type 切换重新拉取,支出和收入用的分类是不同的,比如收入不会有“餐饮交通”这种类目。表单校验放在前端提交前,可以省掉大量无效请求和弹窗骚扰,但后端仍然需要保留校验逻辑,双保险。这里默认收支类型是支出,因为 80% 的记账行为都是支出,用户如果记收入再手动切换,这是产品设计上的一个常见优化。
4.3 统计页图表:画饼图用原生 canvas 还是图表组件
统计页是整个系统的门面,筛选“本月”“上月”“近半年”后,能看到支出分类占比的饼图和各月趋势的柱状图。实现图表有两种主流方案:小程序原生 canvas 手绘,或者引入第三方图表库。原生 canvas 的好处是不依赖任何包,调 canvas 2d 接口画扇区,但代码量很大,一个简单的饼图至少一百行,还要处理 canvas 在不同机型上的尺寸适配,非常容易翻车。我更推荐使用成熟的小程序图表库,比如 echarts 的小程序版,按需引入饼图和柱状图两个组件即可,封装得比较完善,论文里写“前端基于 canvas 实现数据可视化”也没有问题。
// pages/stats/stats.js import * as echarts from '../../ec-canvas/echarts'; Page({ data: { month: '2025-04' }, onLoad() { this.chartComp = this.selectComponent('#pieChart'); this.loadStats(); }, async loadStats() { const res = await request({ url: '/api/bill/stats-by-category', method: 'GET', data: { month: this.data.month, type: 1 } }); if (res.code === 0) { this.initPieChart(res.data); // res.data 是 [{ name, value }] } }, initPieChart(data) { this.chartComp.init((canvas, width, height, dpr) => { const chart = echarts.init(canvas, null, { width, height, devicePixelRatio: dpr }); chart.setOption({ series: [{ type: 'pie', radius: ['40%', '70%'], // 环形饼图 data: data.map(item => ({ name: item.category_name, value: Number(item.total_amount) })) }] }); return chart; }); } });饼图用环形而不是实心饼,能在中间区域留出空间显示总支出金额,这是页面视觉效果提升的关键设置。radius 数组里两个值分别是内半径和外半径的百分比,['40%', '70%'] 就是中间有个洞。数据返回后必须用 Number 把字符串金额转成数字,图表库内部做数值计算,如果传字符串可能导致排序或显示异常。最后,统计页一定要加一个时间选择器,不然每个月都只看当前月,就体会不到“看趋势”的功能了。切换月份时重跑 loadStats,数据刷新后图表需要先 clear 再 setOption,否则会叠加显示。
5. 避坑清单:登录、金额和审核的五个常见问题与排查路径
5.1 真机预览正常,正式版请求全部失败,提示“域名不合法”
现象:开发工具里勾选了“不校验合法域名”后一切正常,一发布体验版或正式版,所有 wx.request 全报 fail,错误信息是“url not in domain list”。原因:微信小程序对网络请求有严格的域名白名单机制,只能在后台配置 request 合法域名,开发时跳过校验只对本地开发工具生效,真机会强制校验。解决:在小程序公众平台后台的“开发管理”里配置 request 合法域名,域名必须是备案过的 HTTPS,不能是 IP,不能带端口号。如果后端还没上线到公网,可以先在流程里改成 wx.request 的 url 全部指向本地局域网 IP,但注意这只是临时方案,真机远程调试时也必须开“不校验合法域名”才能测通。
5.2 代码里金额相加的结果是 19.999999
现象:0.1 加 0.2 得到 0.30000000000000004,更典型的是有一笔 19.9 和一笔 0.1,相加显示的是 19.99999998。原因:JavaScript 的 Number 类型遵循 IEEE 754 标准,二进制浮点数无法精确表达大多数十进制小数,这是语言层面的限制,不算 bug。解决:最好不要在前端做金额的累加计算,统计功能统一由后端 SQL 的 SUM() 完成,后端会返回一个能接受的十进制结果。如果前端确实需要算,用整数分做运算再加单位,或者 Number(total.toFixed(2)) 做一次四舍五入。建议后端在返回统计结果时就把金额转成字符串,前端展示时不参与任何运算。
5.3 两个家庭成员同一时间记账,有一方的账单丢失
现象:媳妇和老公同时保存账单,刷新后只有一条记录,另一条像没提交过一样。原因:请求并发时,先发送的请求还没来得及写入数据库,后发送的请求已经开始执行,数据被覆盖。排查时要确认代码里是否有将 userId 写到 bill 表,确保字段数据正确;更重要的是检查是否存在前端对同一数据源做了缓存更新互相覆盖的问题,以及是否出现了重复提交按钮的情况。解决分两层:前端在 submit 函数入口加一个 let isSubmitting = false 的锁,点击立即置 true,请求完成后置回 false,防止按钮连点导致一条记录提交两次;后端在 bill 表用自增主键,本身不会覆盖历史数据,但如果有更新账单的逻辑,必须带 id 条件。另外建议给表单提交按钮加 loading 状态,让用户直观看到请求正在发送。
5.4 iOS 端日期选择器不显示,Android 正常
现象:picker 组件的 mode="date" 在 iOS 上点击没反应,或者弹出的日期是英文格式。原因:picker 组件在 iOS 上依赖的是系统自带的 ActionSheet 风格选择器,它的初始数据对格式有要求,如果 value 传入的值不是规范日期格式,iOS 端会直接渲染失败。解决:所有传给 picker 的日期值必须严格写成 YYYY-MM-DD 格式。前期开发时用 JS 拼日期容易拼成 YYYY-M-D,比如 2025-4-5,Android 能兼容,iOS 不行。因此格式化函数里 month 和 day 必须用 padStart(2, '0') 补成 04、05。
5.5 小程序审核被拒,理由是“涉及财产信息需要补充说明”
现象:提交审核后收到站内信,拒绝原因类似“你的小程序涉及个人财产信息的收集和使用”。原因:家庭理财系统天然包含用户收入和支出数据,属于敏感个人信息,审核时如果不主动说明,会按违规处理。解决:在小程序后台的“用户隐私保护指引”中,完整声明收集了哪些信息、用途是什么,并在小程序内增加一个可点击的《隐私政策》页面,首次进入时弹窗提示。另外如果设置了体验版账号,要在审核备注里填写测试账号和密码,审核人员才有办法完整体验功能,否则很容易因为“无法完整体验”被驳回。这两项补齐后再提审,通过率会明显提高,这也是我做理财类小程序最大的一个流程心得。
6. 毕业论文章节怎么排:把系统实现写成一章能答辩的内容
论文写不好,系统做得再好也容易吃亏。毕设论文通常包含六章:绪论、相关技术介绍、需求分析、系统设计、系统实现、系统测试。其中系统设计章是整个项目的地基部分,也是答辩时最容易丢分的地方。很多人会犯一个错误:把全部代码贴在论文的附录或系统实现章里,导致关键设计思想被淹没。正确做法是只贴每个模块最关键的一段核心代码,比如登录接口贴 jscode2session 换 openid 的逻辑,统计功能贴 GROUP BY 聚合的 SQL,再配一个简短的设计说明,用文字讲清楚输入输出和数据流,这样评审也能抓住重点。图表素材建议提前准备四个:系统架构图、用例图、数据库 ER 图、核心功能的时序图。画完直接截图放进论文,如果到答辩前加班画会很仓促,尤其是 ER 图,需要和数据库表设计一一对应。
答辩高频问题集中在三个方向上:为什么这个功能要这么设计、性能瓶颈在哪、系统有什么不足。举例来说,老师可能会问“这个系统并发 1000 用户时会不会卡”,哪怕你只是毕设,也得准备一个回答思路:先从数据库连接池数量分析,再加一层 Redis 缓存用户登录态和常用统计结果,把小程序的请求压力挡住。你可以提前做一遍简单的并发测试,用压测工具跑一下登录接口的 QPS,把测试数据写进论文的测试章里,这个问题就有实例支撑。测试用例的设计也要覆盖三个层次:正常输入、边界输入、非法输入,例如金额 0、负数、超大数字,在考察时最好能直接演示这些用例。
做一个全栈项目容易陷入重功能、轻文档的误区。等到最后一次检查时你会发现根本没有时间记录比“代码写完了”更多的信息:表结构改了几版、接口调了哪些参数、踩了哪些坑,这些内容如果没有在开发过程中随手记录下来,写论文时会缺时间线的支撑。每次完成一个模块,记录一张截图加两行说明,论文就像是拼图。我自己的一个习惯是准备一个开发日志文件,每完成一个接口就写下请求方式、参数、响应结构和遇到的坑,这样最后论文中的测试结果和接口文档可直接取材。这个习惯帮我避开了很多答辩现场的措手不及,希望帮到你。
本文还有配套的精品资源,点击获取