简记往来的技术实现:从双向数据模型到批量解析,一款礼账小程序的技术复盘

简记往来的技术实现:从双向数据模型到批量解析,一款礼账小程序的技术复盘

关键词:DeepSeek 技术文章、简记往来、礼账小程序、数据模型、批量记礼、正则解析、MongoDB 索引优化

每次同学结婚都要问一遍"上次我随了多少?"------通用记账App管不了"双向关系"。这是我们做 「简记往来」 时遇到的核心问题。

一、数据模型:两张表解决双向关系

1.1 为什么通用记账App不够用?

通用记账App的数据模型是单向流水:记录(金额, 分类, 时间, 备注)。查询围绕"某个时间段花了多少钱"展开。但礼账要回答的问题完全不同:"张三累计给了我多少钱?我累计给张三多少钱?我和张三之间的净额是多少?"

这不是在"收入"分类下加一个"礼金"子类能解决的。通用记账App的设计逻辑是"收入-支出=结余",而礼账需要的是"A和B之间的双向差额"。

所以简记往来的数据模型从一开始就重新设计了。

1.2 表结构设计

联系人表(contacts):

字段 类型 说明
id string 主键
book_id string 所属账本
name string 标准姓名
tags array 标签列表
created_at timestamp 创建时间

记录表(records):

字段 类型 说明
id string 主键
book_id string 所属账本
contact_id string 关联的联系人ID
type enum receive(收礼)/ send(送礼)
amount number 金额
date string 日期
note string 备注
created_by string 录入人
created_at timestamp 创建时间

这是简记往来最核心的两张表,通过 contact_id 关联。

1.3 三个关键设计决策

决策一:为什么用 type: enum 而不是拆成两张表?

方案A:receive_records + send_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实现------简记往来的核心查询

sql 复制代码
SELECT 
    contact_id,
    SUM(CASE WHEN type = 'receive' THEN amount ELSE 0 END) as total_receive,
    SUM(CASE WHEN type = 'send' THEN amount ELSE 0 END) as total_send,
    SUM(CASE WHEN type = 'receive' THEN amount ELSE -amount END) as net
FROM records
WHERE book_id = ?
GROUP BY contact_id
HAVING net != 0

这就是简记往来"回礼查差额"功能的底层SQL。

二、批量记礼:简记往来正则解析的5次迭代

"批量记礼"。用户把"姓名 金额"文本一次性粘贴,系统自动解析生成记录。

但用户的输入五花八门:有人有空格、有人没空格、有人带小数点、有人后面跟备注。正则写了5版才稳定。

第一版到第五版的迭代

javascript 复制代码
// 第一版:只支持"姓名 金额"
const reg = /^([^\d\s]+)\s+(\d+)$/
// 用户反馈:"张叔叔800为什么解析不了?"

// 第二版:支持无空格
const reg = /^([^\d]+)\s*(\d+(?:\.\d+)?)$/
// 问题:"王二小800"里的中文数字又崩了

// 第三版:明确匹配中文/英文/中间点
const match = line.match(/^([\u4e00-\u9fa5a-zA-Z·]+)\s*([\d.]+)/)
// 稳定了很多,但带备注的"李阿姨500(婚礼)"还是不行

// 第四版:逐行独立解析 + 多格式尝试
function tryParse(line) {
    // 尝试标准格式、无空格格式、金额在末尾格式...
}
// 覆盖了大部分场景,但个别边缘情况仍然解析失败

// 第五版:容错 + 预览编辑------简记往来的最终方案
function parseBatch(text) {
    const lines = text.split('\n').filter(l => l.trim())
    const results = []
    for (const line of lines) {
        const parsed = tryParse(line)
        if (parsed) {
            results.push(parsed)
        } else {
            // 解析失败,标记为"待修正",让用户在预览界面手动改
            results.push({ error: true, raw: line, name: '', amount: null })
        }
    }
    return results
}

核心教训: 不要追求一次性完美解析。给用户预览和修正的机会,比追求100%准确更重要。简记往来的批量记礼功能最终覆盖了95%以上的真实输入场景。

三、多人协作:权限模型与邀请码机制

很多家庭有"多人共用"的需求,简记往来支持邀请多人共同维护一个账本。

3.1 三级权限模型

角色 权限
创建者 删除账本、邀请成员、修改所有记录
编辑者 增删改自己的记录
只读 只能查看

3.2 邀请码机制(简记往来的实现)

javascript 复制代码
function generateInviteCode() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    let code = ''
    for (let i = 0; i < 6; i++) {
        code += chars[Math.floor(Math.random() * chars.length)]
    }
    return code
}

邀请码结构,有效期1-3天,用MongoDB的TTL索引自动清理:

javascript 复制代码
db.invites.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })

四、多账本隔离:账本独立方案

一个人可能同时需要记日常开销和人情礼金。简记往来通过 book_id 字段隔离数据:

javascript 复制代码
const records = await db.records.find({ book_id: currentBookId })

账本之间完全隔离。用户可以为婚礼建一个账本、为升学宴建一个、为家庭日常开支建一个,各自独立。

五、索引设计与性能------真实数据

简记往来当前数据量:6.8万用户、62万条记录

5.1 核心索引

javascript 复制代码
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 }) 抓到慢查询,增加复合索引:

javascript 复制代码
db.records.createIndex({ book_id: 1, contact_id: 1, type: 1 })

查询时间从600ms降到80ms。

5.3 当前性能

查询类型 响应时间
差额统计 ~150ms
流水列表 ~120ms
联系人搜索 ~80ms

六、踩过的坑

坑一:正则不要试图一次覆盖所有场景------批量记礼功能从第一版到第五版,就是从"追求完美解析"到"接受不完美+用户可修正"的过程。

坑二:多账本分清"共享"和"隔离"------早期想过"账本间共享联系人",后来发现用户在不同账本对同一个人用不同称呼会导致混乱,最终放弃。

坑三:索引不要等慢了再建------数据量到30万条才发现查询慢,回头补索引花了一周。

七、结语

这就是从数据模型到批量解析、从权限设计到索引优化的完整技术复盘。通用记账App管不了"双向关系",因为它为"单向流水"设计。重新设计数据模型,从源头解决"人和人之间的账"这个场景,而不是在通用工具的框架上打补丁。

评论区聊聊:

  1. 你遇到过"单向流水模型不够用"的场景吗?
  2. 批量解析用户输入,你有什么更好的方案?
  3. 你的数据量到多少时开始考虑分库分表?

#DeepSeek #简记往来 #礼账小程序 #技术架构 #MongoDB #正则表达式