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

发布时间: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分类 ID
billscategoryName分类名快照,冗余字段
billsnote备注,可为空
billsdate本地日期字符串,如2025-06-01
billscreatedAtdb.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-type="getPhoneNumber">,拿到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 id="chart" type="2d" style="width: 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) }) }) }

第一个关键参数是dpr:canvas 的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 type="2d"和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权限模型。

我现在的习惯是每次动数据库字段前,先把老数据导出备份,再改结构和查询代码,跑完回归链路才发版——这个顺序救过我两次。希望帮到你。

本文还有配套的精品资源,点击获取