微信记账小程序源码开发:技术选型、数据模型与云函数避坑全解析

发布时间:2026/10/28 8:50:38
微信记账小程序源码开发:技术选型、数据模型与云函数避坑全解析 简介一套可直接运行的微信记账小程序完整源码面向正在学习小程序开发、或想快速搭建个人财务工具的开发者。压缩包共41个文件、体积仅45KB以9个js逻辑、7个wxml页面结构、8个wxss样式、7个json配置为主另含png图标与README说明模块划分清晰适合直接导入微信开发者工具逐项研读。源码完整覆盖微信授权登录、收支录入、分类管理、图表统计、日历视图、关键词搜索及云同步等核心能力pages目录下各页面严格按wxml/wxss/js/json四件套组织同时包含全局app.js/app.json/app.wxss配置和utils工具类能够直观体现小程序框架、数据绑定与交互逻辑的配合方式。对云数据库表结构、缓存优化与响应式适配也有一定展示。当前已有326人学习下载尤其适合具备前端基础、希望借助真实项目提升小程序开发能力的读者。1. 微信记账小程序源码为什么它是你该抄的第一份作业如果你是第一次接触微信小程序开发又不想从「你好世界」那种毫无营养的 Demo 起步那记账小程序几乎是市面上最合适的练手项目。功能闭环完整——增删改查、分类统计、图表展示、数据导出全都有业务逻辑不复杂不用接触地图、蓝牙、支付这些高门槛能力最重要的是它贴近真实需求做完能自己用给家人用甚至改改就能接外包。很多开发者把这份源码当作面试作品或接单模板原因就在这里。不过别被「记账」两个字骗了真要把一份记账小程序源码改到能稳定跑起来你会踩到一堆微信平台特有的坑手机号登录、本地存储上限、日期时区偏移、金额浮点精度……这篇就把这些账一笔记清楚。2. 技术选型与工程搭建原生、uniapp 与云开发数据库怎么选2.1 原生小程序和 uniapp 的真实差距拿到一份记账小程序源码先判断它是原生还是 uniapp 工程。看根目录有没有manifest.json——有就是 uniapp 工程没有就是原生小程序。原生小程序的工程结构是pages/、utils/、app.js、app.json这一套。我一般建议新手优先用原生。理由很直接微信开发者工具对原生工程支持最稳断点调试、真机预览都是零成本而且网上能找到的记账小程序源码十有八九是原生写的错了直接搜报错信息就能找到答案。uniapp 的好处是以后能打包成 H5 和 App但代价是你得额外学一套 Vue 语法层遇到样式错位、导航栏适配这类问题排查链路更长属于给自己加难度。打开工程后第一件事不是看代码而是点开发者工具右上角的「详情」确认本地设置里选了「测试号」还是你自己的 AppID。用测试号最省事不用注册小程序账号但云开发能力会被限制后面要跑云函数就得换正式 AppID。2.2 本地缓存还是云开发两条路的成本对比记账小程序的数据存储就两条主流路线选型决定了整个项目的工作量。维度本地缓存wx.setStorageSync云开发云数据库上手成本极低一个 API 搞定读写中需要开通云环境、建集合、写云函数数据安全卸载即丢失换手机不同步存在云端登录后可恢复容量上限单 key 1MB总计 10MB单条记录上限 512KB集合不限条数适合场景个人自用、单机模式多人登录、跨设备同步、接外包交付我的判断是纯自用版本的记账小程序用本地缓存完全够一两年的账单数据也就几百 KB。但如果你打算把源码改造后给别人用或者作为课程设计和面试作品务必走云开发路线。原因是「换手机数据不丢」是所有记账类产品的刚需也是你表达项目完成度的核心卖点。2.3 把源码跑起来的最小步骤假设你手里的源码走云开发路线跑通它只需要四步# 1. 用微信开发者工具导入工程目录 # 导入时 AppID 填自己的小程序 AppID后端服务选「微信云开发」 # 2. 开通云开发环境 cloudbase 控制台 - 新建环境 - 按量付费或包月套餐选最便宜档位即可 # 3. 创建数据库集合 数据库 - 新建集合 - bill账单表 数据库 - 新建集合 - user用户表 # 4. 在 app.js 里确认云环境 ID// app.js 关键片段 App({ onLaunch() { if (!wx.cloud) { console.error(当前基础库版本过低请使用 2.2.3 以上基础库); return; } wx.cloud.init({ // 这里的 env 替换成你自己的云环境 ID控制台首页能看到 env: your-env-id, traceUser: true }); } });这两段必须配合着看。导入工程时如果报「app.json 未找到」说明你选错了目录层级——小程序工程要选到包含app.json的那一层而不是外层打包文件夹。云环境 ID 是新手最容易忽略的翻车点很多源码里会残留作者的 env 配置你不改掉数据会写到别人环境里报错信息还特别隐晦比如「collection not exists」——其实不是集合不存在是根本连错了数据库。2.4 依赖黑匣子npm 安装与构建源码里如果用了 npm 包比如wx-charts、dayjs导入工程后必须做一次 npm 构建。开发者工具菜单栏找到「工具」→「构建 npm」构建完会在工程内生成miniprogram_npm目录。这一步不做真机上会白屏因为 npm 包根本没有被打包进小程序代码包。要注意的是构建 npm 要先把工程根目录的project.config.json里packNpmManually配置确认好否则构建结果不会自动生效。很多源码自带package.json但没有node_modules你需要先在根目录执行npm install再去开发者工具里构建。整个链路是安装依赖 → 构建 npm → 清缓存 → 重新编译四步缺一不可。3. 账单数据模型设计金额、分类与日期别在这三处翻车3.1 数据集合字段先把 schema 定死再写代码记账小程序的核心集合就是账单表bill字段设计直接决定后面统计功能好不好写。我见过很多半成品源码字段只有amount、type、date三个字段短期够用一旦要加账本维度、家庭成员维度就得返工。常见的做法是预留这些字段// 云数据库 bill 集合的推荐结构 { _id: 自动生成不用管, _openid: 云开发自动写入的操作用户 openid做数据隔离, amount: 1250, // 金额以「分」为单位存储后面细说 type: expense, // expense 支出 / income 收入 category: food, // 一级分类 subCategory: lunch, // 二级分类 account: cash, // 支付账户 note: 和同事午饭 AA, // 备注 billDate: 2024-06-13, // 业务日期YYYY-MM-DD 字符串 billTime: 12:30:00, // 业务时间 createTime: 1718262600000, // 创建时间戳用于排序 updatedTime: 1718262600000 }这个结构的关键不是字段多而是billDate和billTime要分开存。很多新手把日期存成new Date()对象或者2024/6/13这种杂格式到后面做按天、按月分组统计时要么正则匹配半天要么时区错乱导致账单归到前一天。日期统一存YYYY-MM-DD字符串这是记账类项目最省心的做法。3.2 金额用「分」存储0.1 0.2 不是 0.3记账小程序里最隐蔽的坑是浮点数精度。JavaScript 里0.1 0.2的结果是0.30000000000000004你直接在页面上渲染会显示一长串小数。所以规范做法是金额统一换算成整数分存储。// utils/money.js 金额转换工具 function toCents(yuan) { // 先把元转成字符串再计算绕开浮点误差 const parts String(yuan).split(.); const yuanPart parseInt(parts[0], 10) || 0; const centPart parts[1] ? parseInt(parts[1].padEnd(2, 0).slice(0, 2), 10) : 0; return yuanPart * 100 centPart; } function toYuan(cents) { const yuan Math.floor(cents / 100); const cent cents % 100; return ${yuan}.${String(cent).padStart(2, 0)}; } module.exports { toCents, toYuan };这段代码有两个细节值得注意。String(yuan).split(.)先把浮点数转成字符串是因为1250.5 * 100在 JS 里可能等于125050.00000000001直接乘会留下误差尾巴padEnd(2, 0)是为了处理用户输入12.5这种只有一位小数的金额——不补零12.5会被换算成102分而不是100分。真机上用户输入千奇百怪12.结尾带点的、12.345三位的都要在输入框层面拦截掉转换函数只做兜底。3.3 分类体系一级分类固定二级分类用户可自定义记账源码里最常见的逻辑短板是分类写死死。用户想加一个「宠物」分类得改代码。我常用的设计是双层分类一级分类固定在代码里标识好餐饮、交通、购物、居住、娱乐、医疗、教育、其他二级分类放在云数据库category集合里用户可增删。账单表只存category和subCategory两个字段不存分类的展示名展示时联表查询或前端映射。// 前端分类常量一级分类固定保证统计口径一致 const EXPENSE_CATEGORIES [ { id: food, name: 餐饮, icon: icon-food, subCategories: [早餐, 午餐, 晚餐, 水果, 零食] }, { id: transport, name: 交通, icon: icon-bus, subCategories: [公交地铁, 打车, 加油, 停车] }, { id: shopping, name: 购物, icon: icon-bag, subCategories: [日用品, 服饰, 数码, 其他] }, // ... ];这里的设计直觉是一级分类对应统计图表上的几个柱子太碎了图表没法看二级分类才对应真实消费场景交给用户自由发挥。如果你把一级分类也做成用户可自定义后面做报表时就会面对几十个分类的混乱局面这是记账产品最典型的「看似灵活、实则失控」设计。3.4 写入账单的完整链路// pages/addBill/addBill.js 简化版新增账单逻辑 const db wx.cloud.database(); const { toCents } require(../../utils/money); Page({ data: { amount: , category: food, subCategory: 午餐, note: }, onAmountInput(e) { // 输入框限制只允许数字和小数点最多两位小数 const raw e.detail.value; if (!/^\d*\.?\d{0,2}$/.test(raw)) return; this.setData({ amount: raw }); }, async onSubmit() { const { amount, category, subCategory, note } this.data; if (!amount || parseFloat(amount) 0) { wx.showToast({ title: 请输入金额, icon: none }); return; } const ts Date.now(); try { await db.collection(bill).add({ data: { amount: toCents(amount), type: expense, category, subCategory, note, billDate: this.getTodayString(), billTime: this.getNowTimeString(), createTime: ts, updatedTime: ts } }); wx.showToast({ title: 记一笔成功, icon: success }); // 记录完成后返回列表页并触发刷新 wx.navigateBack(); } catch (err) { console.error(记账失败, err); wx.showToast({ title: 网络异常请重试, icon: none }); } } });逻辑说明toCents(amount)必须在入库前调用入库的是整数分而不是原始小数正则/^\d*\.?\d{0,2}$/在输入时做第一层拦截从源头挡住12.345这种非法金额输入billDate和billTime用本地时间拼字符串保证用户看到的日期和他感知的日期一致。这里有个细节——不要用Date.now()去做按天分组因为时间戳带时区用户在北京时间 6 月 13 日 00:30 记的账换算成 UTC 可能是 6 月 12 日 16:30按时间戳分组就会归到错误的一天。参数调优建议如果你要加账本功能比如分家庭账本和生意账本在bill集合里加一个ledgerId字段每个新增接口都带上它。别做成多表关联小程序云数据库的关系查询能力很弱能用一个字段解决的不要开一张表。4. 登录与手机号绑定用云函数把 openid 变成用户资产4.1 wx.login 拿 code云函数换 openid记账小程序在没有账号体系时所有用户的数据都混在一起你加了多少条记录、记了哪些账无法区分。云开发的便捷之处在于数据库操作会自动带上_openid字段天然做了数据隔离。但如果你要显示用户手机号、做用户画像就得在登录环节下功夫。完整的登录链路分三步// 第一步前端 wx.login 获取临时 code const loginRes await wx.login(); const { code } loginRes; // 第二步调用云函数换取 openid 并拿到手机号 const cloudRes await wx.cloud.callFunction({ name: login, data: { code } }); // 第三步把用户信息写入本地缓存避免每次启动都重复调用 const { openid, phone } cloudRes.result; wx.setStorageSync(userInfo, { openid, phone });这里有个常见的理解误区也是我见过大量源码里写错的地方wx.login的code不能直接换手机号。2023 年后微信调整了手机号获取方式必须用「手机号快速验证组件」配合getPhoneNumber事件回调通过open-typegetPhoneNumber的按钮触发把e.detail.code传给云函数再调用cloud.openapi.phonenumber.getPhoneNumber才能拿到明文手机号。4.2 云函数里的手机号解密逻辑// cloudfunctions/login/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); exports.main async (event) { const { phoneCode } event; // 如果用户点击了「拒绝授权」phoneCode 为空仍可 login if (phoneCode) { try { const res await cloud.openapi.phonenumber.getPhoneNumber({ code: phoneCode }); return { openid: cloud.getWXContext().OPENID, phone: res.phoneInfo.purePhoneNumber, errMsg: ok }; } catch (err) { console.error(手机号获取失败, err); return { openid: cloud.getWXContext().OPENID, phone: }; } } // 用户没授权手机号也没关系openid 已经能标识用户身份 return { openid: cloud.getWXContext().OPENID, phone: , errMsg: no-phone }; };代码的核心思路是「手机号是可选增强openid 才是用户主键」。很多记账源码把手机号当作必填项用户拒绝授权就卡在登录页——这是产品设计失误。记账场景下用户最在意的是「我的账单存没存好」不是「App 拿不拿得到我手机号」。部署这个云函数时有个参数坑cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })一定要用动态环境如果写死env: your-env-id云函数在跨环境调用数据库时会报权限错误。另外云函数目录要单独右键「在终端中打开」并执行npm install装好wx-server-sdk依赖后再右键「上传并部署云端安装依赖」。上传方式选错了云函数会一直报 module not found。4.3 前端页面的登录态管理// utils/auth.js 登录态工具模块 const USER_INFO_KEY userInfo; async function ensureLogin() { const cached wx.getStorageSync(USER_INFO_KEY); if (cached cached.openid) { return cached; } const loginRes await wx.login(); const cloudRes await wx.cloud.callFunction({ name: login, data: { code: loginRes.code } }); const { openid, phone } cloudRes.result; const userInfo { openid, phone }; wx.setStorageSync(USER_INFO_KEY, userInfo); return userInfo; } module.exports { ensureLogin };写登录逻辑的时候要特别留意时序问题页面onLoad里如果先查数据库再调ensureLogin用户第一眼会看到「加载失败」。常见的做法是在app.js的onLaunch里异步预热登录态页面查询数据库前用await ensureLogin()兜底。真机上弱网环境登录接口可能超时前端要加一个 3 秒超时后的重试按钮避免用户永远停在白屏页。5. 避坑本地存储、时区与 setData 的三个血泪现场5.1 wx.setStorageSync 的明文残骸与 10MB 魔咒很多源码为了图方便会把最近 30 天的账单同步到本地缓存做增量展示这时候最隐蔽的坑来了wx.setStorageSync写入的数据在小程序卸载前一直存在本机。网上能搜到很多「微信数据库解密」工具——不是让你去读别人的数据而是要意识到用户在手机上卸载了 App 之后缓存文件可能还留在系统目录里你在缓存里存了手机号、账单金额明细这些就是裸奔在用户设备上的明文资产。解决思路不是去加密——小程序端密钥白盒化加密了也能被逆向抠出来——而是「能少存就少存能存 openid 就不存手机号能存聚合数据就不存明细」。我只在本地缓存放「最近一次同步时间 当月总计」明细永远从云数据库读。第二个坑是 10MB 上限。单 key 最大 1MB总缓存 10MB 是硬限制。如果你的记账小程序把图片备注、语音备注也缓存进本地很快就会触顶。我踩过一次后加了缓存清理策略每次启动时遍历本地缓存超过 5MB 就删掉最早一个月的明细缓存。5.2 日期偏移new Date(2024-06-13) 的时区陷阱这是记账小程序里翻车率最高的 bug没有之一。new Date(2024-06-13)在 iOS 真机上解析的结果是 UTC 时间等于北京时间当天早上 8 点如果你直接拿这个 Date 对象去getDate()6 月 13 日当天凌晨 0 点到 8 点之间记的账会被归到 6 月 12 日。解决方式我在数据模型那节已经埋了伏笔日期一律用字符串不要new Date// 获取今天的业务日期字符串注意补零 function getTodayString() { const now new Date(); const y now.getFullYear(); const m String(now.getMonth() 1).padStart(2, 0); const d String(now.getDate()).padStart(2, 0); return ${y}-${m}-${d}; } // 按月份过滤账单时不要用时间戳范围直接字符串比较 async function getBillsByMonth(monthStr) { // monthStr 形如 2024-06 const db wx.cloud.database(); const res await db.collection(bill) .where({ billDate: db.RegExp({ regexp: ^${monthStr}, // 匹配 2024-06 开头的日期字符串 options: g }) }) .orderBy(billDate, desc) .limit(20) .get(); return res.data; }正则匹配月份的做法比where里写时间范围更省心。云数据库的db.RegExp在查询YYYY-MM-DD字符串时有天然优势不用算月初月末时间戳也不会撞时区。唯一要注意的是正则查询不走索引数据量超过一万条时性能会下降届时要改成billDate 2024-06-01 billDate 2024-06-30的范围查询配合索引才不卡。5.3 setData 超限与页面白屏很多记账列表页会把一年的账单一次性setData到渲染层导致真机直接卡死。小程序的setData单次最大数据量是 1MB超过后基础库会直接报错页面白屏而且报错信息在开发者工具里不明显真机上一片空白才让人抓狂。// 改造分页加载每次只渲染一屏 const PAGE_SIZE 20; Page({ data: { bills: [], hasMore: true, page: 0 }, async loadBills(reset false) { if (!this.data.hasMore !reset) return; const page reset ? 0 : this.data.page; const db wx.cloud.database(); const res await db.collection(bill) .orderBy(billDate, desc) .skip(page * PAGE_SIZE) .limit(PAGE_SIZE) .get(); const bills reset ? res.data : this.data.bills.concat(res.data); this.setData({ bills, page: page 1, hasMore: res.data.length PAGE_SIZE }); }, onReachBottom() { this.loadBills(); } });这里最关键的设计是「每次 setData 只更新增量」。云数据库的skip分页在数据量大时也有性能问题但对于个人记账的量级一年几千条完全够用。别去自己维护「分页游标」来优化那是几十万条数据才需要考虑的事对记账小程序是过度设计。另外一个相关坑是setData的频繁调用。比如滑选金额、连续切换分类时每动一下都调一次setData真机上会有肉眼可见的卡顿。我一般会把用户操作过程用一个局部变量缓冲起来等用户松手或确认的那一刻再真正setData。5.4 导航栏高度适配与测试号调试的玄学问题整理这份源码时还有个让我卡了一晚上的问题是「微信小程序顶部导航栏高度」。iPhone X 以后的机型有刘海navigationStyle: custom自定义导航栏时状态栏高度在安卓和 iOS 上不一样。如果你的记账源码里把总览页顶部的月份切换器写死在导航栏区域真机上就会和胶囊按钮重叠。// 获取顶部安全距离的常用写法 const { statusBarHeight } wx.getWindowInfo(); const menuButtonRect wx.getMenuButtonBoundingClientRect(); // 自定义导航栏高度 菜单按钮顶部 - 状态栏底部间距 const navBarHeight (menuButtonRect.top - statusBarHeight) * 2 menuButtonRect.height;注意wx.getSystemInfoSync在最新基础库已标记废弃要用wx.getWindowInfo()替代否则低版本真机兼容没问题新版本开发者工具会报 deprecation 警告。这个计算逻辑在不同机型上会有 1-2 像素误差属于正常范围不要去纠结精确到像素。还有一个开发期的玄学问题云函数修改后没有生效。好多人在开发者工具里改完云函数代码直接点「上传并部署」结果真机调的还是旧逻辑。这是微信开发者工具的缓存问题。我现在的习惯是上传前先在云函数目录执行一次wx-server-sdk的npm install上传时选「云端安装依赖」部署完成后在云开发控制台点「测试」跑一次看返回确认无误再回开发者工具联调。6. 进阶技巧用云函数聚合统计把账单变成图表和导出文件6.1 统计接口在云函数里做聚合而不是前端算记账小程序做到最后一定会遇到统计报表需求——月度支出趋势、分类占比、日均消费。这些统计如果在前端做每一次打开页面都要把所有账单拉下来再本地计算数据量上来后性能一塌糊涂。常见的做法是写一个统计云函数在数据库端完成聚合// cloudfunctions/getMonthlyReport/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; exports.main async (event) { const { yearMonth } event; // 传 2024-06 const wxContext cloud.getWXContext(); const openid wxContext.OPENID; const res await db.collection(bill) .aggregate() .match({ _openid: openid, billDate: _.gte(${yearMonth}-01).and(_.lte(${yearMonth}-31)) }) .group({ _id: { category: $category, type: $type }, totalAmount: $.sum($amount), count: $.sum(1) }) .limit(50) .end(); return { stats: res.list }; };这个方案的核心优势是数据只发生一次网络传输云函数返回的就是加工后的统计结果前端拿到后直接渲染图表体积可能只有几 KB。.match里的日期字符串比较配合我在第 3 章埋的billDate字段设计完全绕开时区问题。云函数里聚合查询的group._id支持多字段但要注意$符号引用字段时要对应使用聚合操作符——这里$.sum来自db.command.aggregate别和数据库指令_.sum混了。6.2 导出 CSV一张表撑起「数据资产」感知记账小程序加一个「导出账单」按钮用户的信任感会明显上升——数据能拿走用户才觉得有掌控感。导出 CSV 不需要服务端参与前端把查询结果拼接成字符串写入临时文件后调分享给好友、发到微信文件传输助手即可。// 导出当月账单为 CSV function exportCSV(bills) { // CSV 头 let csvStr \uFEFF日期,分类,金额(元),备注\n; bills.forEach(b { csvStr ${b.billDate},${b.category},${(b.amount / 100).toFixed(2)},${b.note || }\n; }); const fs wx.getFileSystemManager(); const filePath ${wx.env.USER_DATA_PATH}/bill_${Date.now()}.csv; fs.writeFileSync(filePath, csvStr, utf-8); wx.shareFileMessage({ filePath, fileName: 我的账单.csv, success() { wx.showToast({ title: 导出成功, icon: success }); } }); }\uFEFF这个 BOM 头是血泪经验不加它Excel 打开中文 CSV 会乱码。金额在导出时转回元并toFixed(2)保证两位小数别把整数分直接导出用户看不懂 1250 是什么意思。如果导出数据超过 5000 条writeFileSync一次性写入可能卡顿改成流式追加写会稳一点但对个人记账来说 5000 条已经是多年数据的量级了。6.3 我在这份源码上的最后一点个人技巧图表库选型上wx-charts虽然老但胜在稳定注意它在真机上 canvas 渲染偶尔会出现「首次渲染空白」的 bug解决方案是在onReady里延迟 100ms 再初始化这算是个玄学但确实有效的办法。画柱状图时超过 12 个分类建议直接折叠成「其他」聚合分类不然横轴标签会叠字这也是我做了一版报表后反复调整出来的经验。希望这些在源码上踩过的坑能让你的记账小程序少走几段弯路早日跑出第一张自己的月度账单报表。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询