简记往来的技术实现:从双向数据模型到批量解析,一款礼账小程序的技术复盘
简记往来的技术实现从双向数据模型到批量解析一款礼账小程序的技术复盘关键词DeepSeek 技术文章、简记往来、礼账小程序、数据模型、批量记礼、正则解析、MongoDB 索引优化每次同学结婚都要问一遍“上次我随了多少”——通用记账App管不了“双向关系”。这是我们做「简记往来」时遇到的核心问题。一、数据模型两张表解决双向关系1.1 为什么通用记账App不够用通用记账App的数据模型是单向流水记录(金额, 分类, 时间, 备注)。查询围绕“某个时间段花了多少钱”展开。但礼账要回答的问题完全不同“张三累计给了我多少钱我累计给张三多少钱我和张三之间的净额是多少”这不是在“收入”分类下加一个“礼金”子类能解决的。通用记账App的设计逻辑是“收入-支出结余”而礼账需要的是“A和B之间的双向差额”。所以简记往来的数据模型从一开始就重新设计了。1.2 表结构设计联系人表contacts字段类型说明idstring主键book_idstring所属账本namestring标准姓名tagsarray标签列表created_attimestamp创建时间记录表records字段类型说明idstring主键book_idstring所属账本contact_idstring关联的联系人IDtypeenumreceive收礼/ send送礼amountnumber金额datestring日期notestring备注created_bystring录入人created_attimestamp创建时间这是简记往来最核心的两张表通过contact_id关联。1.3 三个关键设计决策决策一为什么用type: enum而不是拆成两张表方案Areceive_recordssend_records两张表。方案B单张records表 type字段区分。方案A的问题是差额统计需要UNION两张表且扩展新类型要改表结构。简记往来选了方案B因为扩展性更好。决策二为什么net净额不存储而是实时计算最开始想把net存下来。但新增或修改一条记录时所有相关联系人的net都要重新计算并发写入容易出脏数据。简记往来改成实时计算用GROUP BY contact_id聚合收礼和送礼配合(book_id, contact_id)复合索引后62万条数据的聚合查询稳定在150ms以内。决策三为什么不冗余存储联系人信息想过直接把联系人姓名冗余到记录表里。但如果一个人改名了所有历史记录都要改——维护成本比连表查询高。简记往来保持规范化设计用contact_id关联查询。1.4 差额统计的SQL实现——简记往来的核心查询SELECTcontact_id,SUM(CASEWHENtypereceiveTHENamountELSE0END)astotal_receive,SUM(CASEWHENtypesendTHENamountELSE0END)astotal_send,SUM(CASEWHENtypereceiveTHENamountELSE-amountEND)asnetFROMrecordsWHEREbook_id?GROUPBYcontact_idHAVINGnet!0这就是简记往来“回礼查差额”功能的底层SQL。二、批量记礼简记往来正则解析的5次迭代“批量记礼”。用户把“姓名 金额”文本一次性粘贴系统自动解析生成记录。但用户的输入五花八门有人有空格、有人没空格、有人带小数点、有人后面跟备注。正则写了5版才稳定。第一版到第五版的迭代// 第一版只支持“姓名 金额”constreg/^([^\d\s])\s(\d)$/// 用户反馈“张叔叔800为什么解析不了”// 第二版支持无空格constreg/^([^\d])\s*(\d(?:\.\d)?)$/// 问题“王二小800”里的中文数字又崩了// 第三版明确匹配中文/英文/中间点constmatchline.match(/^([\u4e00-\u9fa5a-zA-Z·])\s*([\d.])/)// 稳定了很多但带备注的“李阿姨500婚礼”还是不行// 第四版逐行独立解析 多格式尝试functiontryParse(line){// 尝试标准格式、无空格格式、金额在末尾格式...}// 覆盖了大部分场景但个别边缘情况仍然解析失败// 第五版容错 预览编辑——简记往来的最终方案functionparseBatch(text){constlinestext.split(\n).filter(ll.trim())constresults[]for(constlineoflines){constparsedtryParse(line)if(parsed){results.push(parsed)}else{// 解析失败标记为“待修正”让用户在预览界面手动改results.push({error:true,raw:line,name:,amount:null})}}returnresults}核心教训不要追求一次性完美解析。给用户预览和修正的机会比追求100%准确更重要。简记往来的批量记礼功能最终覆盖了95%以上的真实输入场景。三、多人协作权限模型与邀请码机制很多家庭有“多人共用”的需求简记往来支持邀请多人共同维护一个账本。3.1 三级权限模型角色权限创建者删除账本、邀请成员、修改所有记录编辑者增删改自己的记录只读只能查看3.2 邀请码机制简记往来的实现functiongenerateInviteCode(){constcharsABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789letcodefor(leti0;i6;i){codechars[Math.floor(Math.random()*chars.length)]}returncode}邀请码结构有效期1-3天用MongoDB的TTL索引自动清理db.invites.createIndex({expiresAt:1},{expireAfterSeconds:0})四、多账本隔离账本独立方案一个人可能同时需要记日常开销和人情礼金。简记往来通过book_id字段隔离数据constrecordsawaitdb.records.find({book_id:currentBookId})账本之间完全隔离。用户可以为婚礼建一个账本、为升学宴建一个、为家庭日常开支建一个各自独立。五、索引设计与性能——真实数据简记往来当前数据量6.8万用户、62万条记录。5.1 核心索引db.records.createIndex({book_id:1,date:-1})db.records.createIndex({book_id:1,contact_id:1})db.records.createIndex({book_id:1,type:1})5.2 慢查询优化数据量到30万条时差额统计查询变慢了。用db.setProfilingLevel(1, { slowms: 200 })抓到慢查询增加复合索引db.records.createIndex({book_id:1,contact_id:1,type:1})查询时间从600ms降到80ms。5.3 当前性能查询类型响应时间差额统计~150ms流水列表~120ms联系人搜索~80ms六、踩过的坑坑一正则不要试图一次覆盖所有场景——批量记礼功能从第一版到第五版就是从“追求完美解析”到“接受不完美用户可修正”的过程。坑二多账本分清“共享”和“隔离”——早期想过“账本间共享联系人”后来发现用户在不同账本对同一个人用不同称呼会导致混乱最终放弃。坑三索引不要等慢了再建——数据量到30万条才发现查询慢回头补索引花了一周。七、结语这就是从数据模型到批量解析、从权限设计到索引优化的完整技术复盘。通用记账App管不了“双向关系”因为它为“单向流水”设计。重新设计数据模型从源头解决“人和人之间的账”这个场景而不是在通用工具的框架上打补丁。评论区聊聊你遇到过“单向流水模型不够用”的场景吗批量解析用户输入你有什么更好的方案你的数据量到多少时开始考虑分库分表#DeepSeek #简记往来 #礼账小程序 #技术架构 #MongoDB #正则表达式