资讯详情

微信记账小程序源码:云开发全栈实战与避坑指南

📅 2026/10/28 18:06:45 | 华诺云谱 👁 阅读
微信记账小程序源码:云开发全栈实战与避坑指南
简介一套微信记账小程序完整源码面向微信小程序初学者与进阶开发者适合需要掌握记账类工具从页面搭建到数据统计完整实现的人群可直接用于二次开发、课程设计或学习参考。压缩包共四十一个文件、约四十五KB以JavaScript逻辑文件、WXML页面结构、WXSS样式、JSON配置为主并含wxcharts图表库、工具类与README说明JavaScript处理业务交互、WXML定义页面、WXSS美化样式、JSON进行页面配置目录按模块划分便于查阅。已有三百二十六人学习下载。源码覆盖微信授权登录、收支录入、分类管理、图表统计、日历视图与搜索筛选等常见功能完整展示四件套页面组织方式并涉及微信云开发存储、数据同步、安全加密与性能优化等要点方便开发者拆解功能模块、理解数据库设计思路为自建记账应用提供可靠参照。1. 微信记账小程序源码会记账的人不一定看得懂这套工程微信记账小程序源码拆开看就是一个跑在微信里的记账小程序首页看本月花销点加号记一笔月底看统计图再设个预算上限。做过的人都知道难点不在那几个页面上而在数据怎么存、权限怎么控、统计怎么做才不卡。这套源码把微信云开发那条路走通了不用自己买服务器、配域名、管 HTTPS适合一个人从零跑通一个完整的小程序全栈。新手能跟着把工程跑起来熟手可以直接改字段和聚合逻辑。下文不绕概念从工程结构一路讲到参数和坑照着落地就行。2. 记账小程序架构选型为什么我把它押在微信云开发上记账小程序本质是个典型的微信小程序开发工程就两种技术路线自建后端或者用微信云开发。这个项目的规模决定了它不该上来就上服务器。个人记账场景并发低、数据量小、用户就自己加几个家人用云开发能把开发周期压缩到一周以内。这一章先把架构逻辑讲清楚后面贴代码时你才知道每条命令在回答什么问题。2.1 前后端一体云开发省掉的不只是服务器自建后端最常见的组合是 Nginx MySQL Redis 加一台云服务器再加域名备案和 HTTPS 证书。这套流程对一个小程序来说太重了光是备案等待就能耗掉半个月。云开发把数据库、云函数、存储和鉴权打包在一个环境里客户端直接调wx.cloud.database()服务端逻辑写在云函数里冷热数据都有人管。对比项自建后端微信云开发部署链路买服务器、装环境、配域名、备案开发者工具里开通即用登录鉴权自己维护 session / tokenopenid 自动下发数据库自建 MySQL写接口云数据库客户端直查成本按月租服务器免费额度对个人记账足够适用边界多端、复杂业务纯微信生态、中小数据量我一般不劝人无脑上云开发。如果产品要同时出 App 和 Web云开发会把你锁死在微信生态里那不如一开始就自建后端。但记账小程序只活在微信里云开发就是性价比最高的选项。2.2 源码目录拆解app.json 和四个页面各管什么拿到这套微信记账小程序源码先别急着点运行把目录结构过一遍├── app.js # 全局逻辑云开发初始化在这里 ├── app.json # 页面注册、窗口样式、tabBar 配置 ├── app.wxss # 全局样式 ├── pages/ │ ├── index/ # 首页本月汇总 最近账单列表 │ ├── add/ # 记账页金额、分类、备注 │ ├── stats/ # 统计页按天聚合 图表 │ └── mine/ # 我的预算设置、分类管理 ├── utils/ │ ├── format.js # 金额/日期格式化 │ └── categories.js # 内置分类常量 ├── components/ │ └── bill-item/ # 账单列表项组件 └── cloudfunctions/ ├── login/ # 登录云函数取 openid └── updateBill/ # 改账云函数服务端校验app.json决定了小程序从哪个页面启动。记账小程序通常是单页 tab 结构首页、统计、我的三个 tab记账页用普通页面跳转进去好处是记完一笔wx.navigateBack()就能回到首页刷新。分类和金额格式化放utils是因为多个页面都要用不建议把分类表直接建在数据库里理由在第五章讲。2.3 三个数据库集合bills、categories、users 字段怎么定数据库设计是记账小程序最值得花时间的部分比页面 UI 重要得多。字段一旦定错后面聚合查询和图表全要返工。集合字段说明bills_openid自动写入标识数据归属billsamount金额单位是分整数billstype0 支出 / 1 收入billscategoryId分类 IDbillscategoryName分类名快照冗余字段billsnote备注可为空billsdate本地日期字符串如2025-06-01billscreatedAtdb.serverDate()排序用usersopenid和_openid对齐usersmonthBudget月度预算单位分两个容易忽略的决定一是amount必须存分否则浮点精度问题会在统计页炸给你看二是date存本地日期字符串而不是 Date 对象这样第四章的聚合查询能用字符串比较做范围过滤。categoryName属于典型的设计冗余——记账那一刻把分类名复制进账单之后分类改名、删除都不会影响历史数据。这个取舍在 5.4 会有对应踩坑案例。3. 跑通第一笔账初始化、登录授权与记账写入架构定完开始动手。这一章的目标是让源码在你自己账号下跑通从开发者工具导入工程到页面上成功写入第一笔账单。3.1 创建项目并配置云开发环境 ID 放错一切白搭打开微信开发者工具导入源码目录AppID 选自己的小程序。然后点工具栏的「云开发」按钮开通后创建一个环境把环境 ID 复制出来。这个 ID 形如bookkeeping-3gx1abc要填进app.js// app.js App({ onLaunch() { if (!wx.cloud) { console.error(基础库过低需 2.2.3 及以上版本) return } wx.cloud.init({ env: bookkeeping-3gx1abc, // 换成你自己的环境 ID traceUser: true }) } })这里最容易翻车的地方是env填错或留空。留空时云开发会默认用第一个环境本地只有一个环境没问题一旦你手滑创建了第二个环境所有查询都会落到错误的数据源上。我的习惯是每次新建项目都显式填env不依赖默认值。traceUser打开后可以在云开发控制台看到用户访问记录对排查问题有帮助真机调试时建议保留。3.2 登录与获取手机号openid 才是真正的身份证微信小程序登录这一环云开发已经把脏活干完了。客户端不需要wx.login拿 code 再去换 openid直接在云函数里取上下文// cloudfunctions/login/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async () { const { OPENID, APPID, UNIONID } cloud.getWXContext() return { openid: OPENID, appid: APPID, unionid: UNIONID || } }客户端调用只需要一行const res await wx.cloud.callFunction({ name: login }) wx.setStorageSync(openid, res.result.openid)注意DYNAMIC_CURRENT_ENV这个写法云函数部署后会自动指向当前环境避免把环境 ID 硬编码进函数。unionid只有小程序绑定了微信开放平台才会返回个人项目基本用不到别指望它。关于微信小程序登录获取手机号记账场景默认不需要手机号openid 已经能唯一标识用户。如果你的业务硬要前端放一个button open-typegetPhoneNumber拿到e.detail.code后传给云函数云函数拿 access_token 调微信的getuserphonenumber接口换手机号。要泼一盆冷水的是个人主体小程序调这个接口基本没权限必须认证过的非企业主体。注意拿不到手机号就降级成「openid 昵称」别让登录流程卡住用户。3.3 记账页落地分类选择、金额校验与 bills 写入记账页是核心页面。这里的内置分类我建议放在代码常量里而不是数据库省掉一次网络请求也避开权限问题// utils/categories.js module.exports [ { id: food, name: 餐饮, type: 0 }, { id: traffic, name: 交通, type: 0 }, { id: shopping, name: 购物, type: 0 }, { id: salary, name: 工资, type: 1 } ]记账页逻辑// pages/add/add.js const db wx.cloud.database() const categories require(../../utils/categories.js) Page({ data: { categories, amount: , categoryId: , note: }, onAmountInput(e) { // 只保留数字和一个小数点防止用户输入 12.34.56 const amount e.detail.value.replace(/[^\d.]/g, ) this.setData({ amount }) }, async submit() { const amount Math.round(parseFloat(this.data.amount) * 100) if (!amount || amount 0) { wx.showToast({ title: 金额没填对, icon: none }) return } const category this.data.categories.find(c c.id this.data.categoryId) if (!category) { wx.showToast({ title: 先选个分类, icon: none }) return } await db.collection(bills).add({ data: { amount, // 单位是分 type: category.type, categoryId: category.id, categoryName: category.name, // 快照冗余 note: this.data.note.trim(), date: formatDate(new Date()), // 本地日期字符串 createdAt: db.serverDate() } }) wx.navigateBack() } })三个细节值得说明。第一Math.round(parseFloat(...) * 100)必须拆开写不能写成parseFloat(this.data.amount * 100)后者在小数运算上会引入浮点误差。第二categoryName在写入那一刻做快照后面分类随便改都不影响历史账单。第三db.serverDate()由数据库生成时间避免客户端本地时间不准导致排序混乱。这里的校验只是前端第一道防线真正的校验要在云函数里做第六章会讲。4. 统计报表的实现聚合查询、手绘图表与预算提醒记账不统计等于白记。这一章解决三件事月度数据怎么汇总、图表怎么画、预算提醒怎么写才省钱。4.1 月度聚合用 aggregate 按天分组别在客户端 for 循环里累加最省事的统计方式是把 bills 全查出来在 JS 里 for 循环累加。数据量小的时候看不出问题账目超过几百条每次切 tab 都要白屏一两秒。正确姿势是让数据库做聚合// pages/stats/stats.js const db wx.cloud.database() async function getMonthStat(year, month) { const _ db.command const $ db.command.aggregate const start ${year}-${String(month).padStart(2, 0)}-01 const end ${year}-${String(month).padStart(2, 0)}-31 const { list } await db.collection(bills).aggregate() .match({ date: _.gte(start).and(_.lte(end)) // 字符串范围比较 }) .group({ _id: $date, // 按天分组 total: $.sum($amount) // 每天的支出合计 }) .sort({ _id: 1 }) // 日期升序 .end() return list }这里的关键是date字段存的是YYYY-MM-DD格式字符串gte / lte按字典序比较正好等于日期范围。如果当初存了 Date 对象这一步就要先做时区转换麻烦很多。list里每项长这样{ _id: 2025-06-01, total: 4560 }。客户端聚合适合数据量在几千条以内的个人记账账目再多就把这段代码搬进云函数客户端调wx.cloud.callFunction拿结果。另外group 结果为空时list是空数组前端渲染图表前要兜底不然画图时会报Math.max(...[])得到-Infinity。4.2 图表不依赖第三方canvas 手绘柱状图的三个关键参数小程序图表常用 eCharts 的 weapp 版本但对一个柱状图来说引入整个 eCharts 包太笨重。手绘 canvas 图其实不难难在三个参数上!-- pages/stats/stats.wxml -- canvas idchart type2d stylewidth: 100%; height: 320rpx;/canvas// utils/chart.js function drawBarChart(selector, list) { const query wx.createSelectorQuery() query.select(selector).fields({ node: true, size: true }).exec((res) { if (!res || !res[0]) return const canvas res[0].node const ctx canvas.getContext(2d) const width res[0].width // 逻辑像素 const height res[0].height const dpr wx.getWindowInfo().pixelRatio canvas.width width * dpr // 物理像素 canvas.height height * dpr ctx.scale(dpr, dpr) const bottomPad 24 // 给日期文字留白 const topPad 16 const values list.map(item item.total) const max Math.max(...values, 1) // 兜底除零 const slot width / values.length const barArea height - bottomPad - topPad list.forEach((item, i) { const barWidth slot * 0.6 const barHeight (item.total / max) * barArea const x i * slot (slot - barWidth) / 2 ctx.fillStyle #4A90D9 ctx.fillRect(x, height - bottomPad - barHeight, barWidth, barHeight) ctx.fillStyle #666 ctx.font 10px sans-serif ctx.textAlign center ctx.fillText(item._id.slice(8), x barWidth / 2, height - 8) }) }) }第一个关键参数是dprcanvas 的width / height必须乘上设备像素比再赋给画布否则在真机上文字和柱子全是糊的。第二个是bottomPad坐标轴文字要预留空间柱子不能顶着画布底部。第三个是max传Math.max(...values, 1)而不是Math.max(...values)空数据时会画出一条高度为零的柱而不是 NaN。wx.getWindowInfo()是基础库 2.20.1 以上推荐的 API低版本要回退到wx.getSystemInfoSync()否则真机会报错。4.3 预算提醒把汇总结果缓存起来读库次数省一半预算提醒的逻辑不复杂难在别让每次切页面都去全表聚合。云开发按读次数计费首页onShow每次聚合都在烧钱。常见做法是分层// pages/index/index.js async function refreshSummary() { // 1. 先读缓存缓存命中就不查库 const cached wx.getStorageSync(monthSummary) const today formatDate(new Date()) if (cached cached.date today) { this.setData({ spent: cached.spent }) this.checkBudget(cached.spent) return } // 2. 缓存过期跑一次聚合 const spent await getMonthSpent(new Date()) wx.setStorageSync(monthSummary, { date: today, spent }) this.checkBudget(spent) } function checkBudget(spent) { const budget wx.getStorageSync(monthBudget) || 0 if (!budget) return if (spent / budget 0.8) { wx.showToast({ title: 本月已花 80%, icon: none }) } }缓存键是「日期 汇总结果」同一天内不管打开多少次首页数据库只查一次。预算值本身也放本地缓存用户在「我的」页修改预算时再wx.setStorageSync同步。这个组合能把账单列表之外的读请求压到一天一次个人记账场景完全够用。5. 记账小程序源码避坑金额精度、时区与数据权限这一章是血泪经验合集。前四章照着写能跑通但上线后真正让你半夜爬起来改代码的基本都是这几个问题。5.1 金额 0.1 0.2 ≠ 0.3全程用「分」记账展示再除以 100现象记了三笔 0.1、0.2、0.1 的账统计页显示 0.4000000000000001图表底部坐标轴一串小数。原因JS 的 Number 类型是 IEEE 754 双精度浮点0.1 0.2 这种运算本身就不精确。只要金额在 JS 里以「元」为单位做加减聚合误差就一定会出现只是时间问题。解决数据库amount字段存整数分前端输入时Math.round(parseFloat(...) * 100)转分展示时(amount / 100).toFixed(2)转回元。聚合和预算比较全都用分只有最后渲染到 UI 的一瞬间才做除法。5.2 账单日期少一天时区是元凶date 字段存本地字符串现象6 月 1 日凌晨 0 点半记的账账单列表里显示 5 月 31 日统计图里也跟着跑到上个月去了。原因小程序云数据库存 Date 对象时按 UTC 时间。客户端在 0:30 生成new Date()格式化时用了toISOString()取到的是 UTC 时间比东八区慢 8 小时日期自然回退了一天。越是凌晨记账越容易踩中。解决记账时直接存本地格式化字符串YYYY-MM-DD不落 Date 对象。需要排序和精确到秒的字段比如createdAt单独用db.serverDate()不要和业务日期混用。聚合查询按字符串范围过滤正好和第四章的gte / lte配合。5.3 权限翻车控制台插的数据读不到改全公开又会泄露现象在云开发控制台的数据库里手工插了几条分类小程序里查出来是空数组。有人图省事把集合权限改成「所有用户可读」结果用户能遍历别人的账单。原因云数据库默认权限是「仅创建者可读写」服务端按_openid判断数据归属。控制台手工插入的数据没有_openid客户端永远读不到。而把权限放开成「所有用户可读」等于是把账单隐私数据挂在公网上。解决内置分类这类公共数据直接放代码常量别进数据库。必须入库的共享数据用云函数写入。自定义安全规则可以精确控制{ read: doc._openid auth.openid, write: doc._openid auth.openid }账单是隐私数据我的原则是宁可查不到不能敞开读。你还得知道_openid这个字段在客户端写入时是自动注入的在云函数写入时不会自动注入需要手动从getWXContext()里取出来塞进 data否则云函数插入的账单谁都读不到。5.4 分类删了历史账单全变「未分类」现象把「交通」分类删掉过往所有交通账单的分类显示变空白统计图表少了一块。原因账单只存了categoryId分类一删关联就断了。云数据库没有外键和 join这种问题在客户端层面无解。解决两个方案叠加。第一记账时冗余存categoryName快照第三章已经做了删除分类后历史账单至少还显示文字。第二分类不做物理删除用isDeleted: true软删用户看不到但数据还在。如果支持分类改名记得用云函数批量更新历史账单里的快照where({ categoryId: oldId }).update({ data: { categoryName: newName } })。5.5 模拟器正常、真机白屏基础库、云函数超时与手机号资质现象开发者工具里一切正常真机一打开首页就白屏或者云函数转圈十几秒后报超时。原因三件事最容易同时踩中。一是基础库差异低版本微信不支持canvas type2d和wx.getWindowInfo()代码直接在真机上抛异常。二是云函数冷启动慢默认超时时间太短第一次调用就失败。三是自定义导航栏在 iPhone 刘海屏上高度不一致底部 tab 或者顶部标题被系统状态栏盖住。解决真机模式下打开调试面板看 console别在模拟器里猜。云函数超时在云开发控制台调大到 10 到 20 秒。自定义导航栏时顶部高度要用wx.getMenuButtonBoundingClientRect()去量胶囊位置不同机型的刘海屏差距能到 20px 以上别写死一个像素值。获取手机号那一步个人主体在小程序后台直接就会被拦截上线前先确认主体资质。6. 让源码长出你自己的记账习惯校验、回归与两个迭代方向6.1 云函数做最后一道校验服务端别信客户端的金额客户端校验挡得住手滑挡不住恶意请求。改账这类操作我习惯在云函数里再校验一遍// cloudfunctions/updateBill/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 amount Math.round(parseFloat(event.amount) * 100) if (!Number.isFinite(amount) || amount 0) { return { ok: false, reason: 金额不合法 } } const res await db.collection(bills) .where({ _id: event.id, _openid: OPENID }) .update({ data: { amount, categoryId: event.categoryId } }) return { ok: res.stats.updated 1 } }where条件里带上_openid等于在服务端又加了一道权限闸门就算客户端伪造请求也只能改自己的账。6.2 一条最小回归链路和两个迭代方向每次改完源码我都会跑一遍这条链路记一笔支出 → 改金额 → 删掉 → 回首页看统计是否同步。三个断言分别是列表条数、月度合计、图表最大值。这比写单元测试实在记账小程序的逻辑就集中在「写入、聚合、渲染」这三个环节。两个我认为值得投入的迭代方向一是周期账房租、会员、话费按月重复记账用云函数定时器自动生成账单这是记账类小程序留存率最高的功能二是多账本家庭账本、旅行账本分开统计注意每个账本的成员权限要单独建集合 控制别复用 bills 的_openid权限模型。我现在的习惯是每次动数据库字段前先把老数据导出备份再改结构和查询代码跑完回归链路才发版——这个顺序救过我两次。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑