
简介这是一款面向红白喜事办宴场景的电子礼簿记账工具适合需要操办婚礼、满月宴、寿宴或白事并希望高效管理礼金的用户。它支持创建多个独立事项每个事项可单独设置管理密码兼顾隐私与多场次管理需求。压缩包共约2000个文件以1408个js脚本、354个md说明、212个json配置为主另含少量html、py、xml等整体约609.58MB属于可直接运行的桌面应用工程。功能上覆盖礼金录入、支付方式与备注记录、自动统计总人数与总金额并提供搜索筛选排序、竖排礼单展示、字体颜色背景自定义以及Excel与PDF导出打印增强部分还加入语音播报、内联修改留痕、分批打印、数据加密备份恢复和统计分析。数据本地存储无需联网即可使用。目前已有467人学习下载适合想用数字化方式替代传统纸笔礼簿、追求仪式感与效率兼顾的办宴人群。1. 电子礼簿记账工具 3.0多事项管理、独立密码与 PDF/Excel 导出打印怎么落地村里办一场婚礼收礼台三个人轮班一个记名字、一个收钱、一个复核散场后对账发现少了三百块谁也说不出是哪一笔出的问题。这种场景做红白喜事的人太熟了。电子礼簿记账工具 3.0 要解决的就是这件事把纸质礼簿换成可检索、可导出、可打印的电子账本同时支持创建多个事项——婚礼、满月宴、寿宴、白事各建一个独立账本每个账本单独设管理密码互不串数据。它面向的是操办红白喜事的家庭主事人、乡村宴席一条龙服务队、以及需要长期维护人情往来的记账人员。核心诉求就四个多人分头录入不冲突、金额和姓名能快速核对、结束后能导出 PDF 存档和 Excel 做二次统计、纸质礼簿要能直接打印装订。下面按「选型理由 → 数据结构 → 导出打印 → 避坑 → 进阶」的顺序把一套可复现的方案讲透。2. 多事项账本怎么设计从数据模型到独立密码2.1 为什么用「事项 ID 独立密码哈希」而不是多文件很多人第一反应是「一个事项存一个 Excel 文件简单」。我一开始也这么干过结果翻车在三个地方一是文件散落在桌面改名、移动、误删之后根本对不上二是每个文件都要单独设密码Excel 的打开密码和编辑密码是两套机制用户经常只设一个等于没锁三是跨事项统计「今年一共收了多少礼」时得手动合并十几个文件。正确做法是单库多事项。所有事项存在同一张events表里每条礼金记录通过event_id归属到具体事项。密码不落在业务表里单独一张event_auth表存哈希。这样做的直接好处是切换事项只是换一个event_id查询条件导出、统计、备份都是对同一个库操作不会出现「文件找不到」的玄学问题。提示密码只存哈希不存明文。哪怕数据库文件被人拷走也拿不到原始密码。2.2 建表语句与字段说明下面这套表结构是我实际用过的精简版SQLite 就能跑单机、U 盘、局域网共享都合适。-- 事项表一个婚礼/满月宴/白事就是一条记录 CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 事项名称如2024-10-01 张府婚礼 event_type TEXT NOT NULL, -- 类型wedding/fullmoon/funeral/birthday event_date TEXT, -- 办事日期ISO 格式 2024-10-01 host_name TEXT, -- 主家姓名 created_at TEXT DEFAULT (datetime(now,localtime)) ); -- 密码表与事项一对一存哈希和盐 CREATE TABLE event_auth ( event_id INTEGER PRIMARY KEY, pwd_hash TEXT NOT NULL, -- PBKDF2 或 bcrypt 结果 salt TEXT NOT NULL, updated_at TEXT, FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE ); -- 礼金明细表核心业务数据 CREATE TABLE gifts ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER NOT NULL, guest_name TEXT NOT NULL, -- 送礼人姓名 amount REAL NOT NULL, -- 金额单位元 gift_type TEXT DEFAULT cash, -- cash 现金 / goods 实物 / wechat 转账 relation TEXT, -- 关系亲戚/同事/邻居 table_no TEXT, -- 桌号方便回礼核对 remark TEXT, -- 备注 operator TEXT, -- 录入人多人轮班时必填 created_at TEXT DEFAULT (datetime(now,localtime)), FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE ); CREATE INDEX idx_gifts_event ON gifts(event_id); CREATE INDEX idx_gifts_name ON gifts(event_id, guest_name);字段里有三个是血泪经验加进去的operator记录谁录的对账出问题时能定位到人table_no桌号散场后按桌核对比按时间顺序核对快得多gift_type区分现金和实物实物礼烟酒、被面不能直接算进金额合计否则统计会虚高。2.3 独立密码的设置与校验逻辑密码校验不能只做一次字符串比较就放行要防暴力尝试。我一般会在event_auth之外再加一张失败计数表或者直接在应用层用内存计数加延时。import hashlib, os, hmac def make_password(password: str): 生成盐和哈希存入 event_auth salt os.urandom(16) dk hashlib.pbkdf2_hmac(sha256, password.encode(), salt, 200_000) return dk.hex(), salt.hex() def verify_password(password: str, stored_hash: str, stored_salt: str) - bool: 校验密码用 compare_digest 防时序攻击 dk hashlib.pbkdf2_hmac( sha256, password.encode(), bytes.fromhex(stored_salt), 200_000) return hmac.compare_digest(dk.hex(), stored_hash)参数说明迭代次数 200000 是当前单机场景下兼顾安全和响应速度的常用值太低如 10000容易被离线爆破太高如 1000000在老机器上校验要等一两秒收礼现场体验差。hmac.compare_digest是必须的普通比较会在第一个不同字符处返回理论上可被时序分析虽然礼簿场景威胁不大但这是习惯问题。切换事项时先弹密码框校验通过才把current_event_id写进会话后续所有查询都带上这个 ID。这样即使两个人同时开着软件各自进各自的事项数据也不会串。3. 礼金录入与多人协作把现场对账的坑堵住3.1 录入界面必须做的三个约束收礼台是嘈杂环境录入的人手忙脚乱界面约束比功能丰富更重要。我踩过的坑是一开始做了个自由表单结果出现「张三」和「张 三」被当成两个人金额输成「5000」实际是「500」。后来加了三条硬约束第一姓名输入框做去空格处理并在失焦时提示「该事项下已存在同名记录是否合并查看」。第二金额输入框只允许数字和小数点且实时显示大写金额伍佰元整防止多输零。第三提交前弹一次确认把姓名、金额、桌号用大字号显示确认后才写库。def add_gift(conn, event_id, guest_name, amount, operator, **kw): guest_name guest_name.strip().replace( , ) if amount 0 or amount 1_000_000: raise ValueError(金额超出合理范围) # 同名提醒不阻止只返回提示 dup conn.execute( SELECT COUNT(*) FROM gifts WHERE event_id? AND guest_name?, (event_id, guest_name)).fetchone()[0] conn.execute( INSERT INTO gifts(event_id,guest_name,amount,operator,table_no,remark) VALUES(?,?,?,?,?,?), (event_id, guest_name, amount, operator, kw.get(table_no), kw.get(remark))) conn.commit() return {duplicate: dup 0}逻辑说明同名不阻止写入因为确实可能一家两口都来送礼但返回duplicate标志让界面提示由录入人判断。金额上限设 100 万是防手滑红白喜事单笔极少超过这个数。3.2 多人同时录入的并发处理如果只是单机软件这块可以跳过。但一条龙服务队经常是两台笔记本同时录最后合并。两种方案一是局域网共享同一个 SQLite 文件不推荐SQLite 的写锁在并发高时会报database is locked二是各自录各自的最后用「事项合并」功能按姓名金额去重合并。我一般推荐第二种因为现场网络不稳定共享文件一旦锁死整个收礼台停摆。合并逻辑如下def merge_events(conn, target_id, source_id): 把 source 事项的礼金合并进 target按姓名金额桌号去重 rows conn.execute( SELECT guest_name,amount,table_no,remark,operator FROM gifts WHERE event_id?, (source_id,)).fetchall() inserted 0 for r in rows: exists conn.execute( SELECT 1 FROM gifts WHERE event_id? AND guest_name? AND amount? AND IFNULL(table_no,)IFNULL(?,), (target_id, r[0], r[1], r[2])).fetchone() if not exists: conn.execute( INSERT INTO gifts(event_id,guest_name,amount,table_no,remark,operator) VALUES(?,?,?,?,?,?), (target_id, r[0], r[1], r[2], r[3], r[4])) inserted 1 conn.commit() return inserted去重键选「姓名金额桌号」而不是只用姓名是因为同名不同金额的情况真实存在。合并后要人工复核一遍软件只能做到「疑似重复」提示最终判断还得靠人。3.3 实时合计与分桌统计收礼过程中主家最关心两个数当前总金额、已收多少笔。这两个数用 SQL 聚合实时算不要在前端累加避免多端不一致。-- 总览 SELECT COUNT(*) AS 笔数, SUM(amount) AS 总额 FROM gifts WHERE event_id ? AND gift_type cash; -- 分桌统计方便散场后按桌核对 SELECT IFNULL(table_no,未分桌) AS 桌号, COUNT(*) AS 笔数, SUM(amount) AS 小计 FROM gifts WHERE event_id ? GROUP BY table_no ORDER BY table_no;gift_type cash这个条件是关键实物礼不计入金额合计但要在另一个视图里单独列出回礼时按实物清单准备。4. PDF 与 Excel 导出打印格式、分页和中文乱码4.1 Excel 导出用 openpyxl 而不是 csv导出 Excel 的需求有两个层次一是给主家存档二是给会算账的亲戚做二次统计比如按关系分类汇总。CSV 打开时中文容易乱码且没有格式所以直接用 openpyxl 写 xlsx。from openpyxl import Workbook from openpyxl.styles import Font, Alignment def export_excel(conn, event_id, path): wb Workbook() ws wb.active ws.title 礼金明细 headers [序号, 姓名, 金额, 类型, 关系, 桌号, 备注, 录入人, 时间] ws.append(headers) for c in ws[1]: c.font Font(boldTrue) c.alignment Alignment(horizontalcenter) rows conn.execute( SELECT guest_name,amount,gift_type,relation,table_no,remark,operator,created_at FROM gifts WHERE event_id? ORDER BY id, (event_id,)).fetchall() for i, r in enumerate(rows, 1): ws.append([i, r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7]]) # 合计行 total sum(r[1] for r in rows if r[2] cash) ws.append([, 合计(现金), total]) ws.append([, 总笔数, len(rows)]) # 列宽避免姓名被截断 for col, w in zip(ABCDEFGHI, [6, 14, 10, 8, 10, 8, 20, 10, 20]): ws.column_dimensions[col].width w wb.save(path)参数说明Font(boldTrue)让表头加粗打印出来一眼能分清列宽里姓名给 14、备注给 20是因为中文姓名三四个字加上可能的称谓窄了会显示成####。合计行只累加cash类型和前面统计口径保持一致。4.2 PDF 导出reportlab 处理中文与分页PDF 的难点从来不是生成而是中文。reportlab 默认字体不含中文直接写会变成黑方块。必须注册一个中文字体。from reportlab.lib.pagesizes import A4 from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph from reportlab.lib.styles import ParagraphStyle from reportlab.lib import colors # 注册中文字体Windows 用 simheiLinux 换成 Noto Sans CJK pdfmetrics.registerFont(TTFont(CN, C:/Windows/Fonts/simhei.ttf)) def export_pdf(conn, event_id, event_name, path): doc SimpleDocTemplate(path, pagesizeA4, topMargin40, bottomMargin40) style ParagraphStyle(cn, fontNameCN, fontSize10, leading14) title ParagraphStyle(t, fontNameCN, fontSize16, alignment1, spaceAfter12) rows conn.execute( SELECT guest_name,amount,table_no,remark FROM gifts WHERE event_id? ORDER BY id, (event_id,)).fetchall() data [[序号, 姓名, 金额, 桌号, 备注]] for i, r in enumerate(rows, 1): data.append([str(i), r[0], f{r[1]:.0f}, r[2] or , r[3] or ]) table Table(data, colWidths[40, 100, 80, 60, 180], repeatRows1) table.setStyle(TableStyle([ (FONTNAME, (0, 0), (-1, -1), CN), (FONTSIZE, (0, 0), (-1, -1), 10), (GRID, (0, 0), (-1, -1), 0.5, colors.grey), (BACKGROUND, (0, 0), (-1, 0), colors.whitesmoke), (ALIGN, (2, 1), (2, -1), RIGHT), ])) doc.build([Paragraph(event_name 礼金明细, title), table])关键参数repeatRows1让表头在每页顶部重复打印装订后翻页不会丢失列名colWidths总和要小于 A4 可用宽度约 515pt否则表格会溢出到页面外registerFont的路径在 Windows 和 Linux 不同部署时要按实际系统改这是最常见的翻车点。4.3 打印设置边距、字号和装订线导出 PDF 只是第一步打印出来能装订成册才算完成。A4 纵向、上下边距 40pt、左右 36pt 是常用值。字号 10pt 在 A4 上一行能放约 40 个中文字符姓名和金额都够。如果礼金笔数超过 500建议分两栏或改成 A3否则页数太多。注意打印前先在 PDF 阅读器里预览分页确认没有一行被从中间切断。表格行高固定时长备注会撑高行导致分页错位。5. 避坑与排查导出打印最容易翻车的五个地方5.1 现象Excel 打开后中文全是乱码原因用 csv 模块写文件时没指定encodingutf-8-sigExcel 默认按 GBK 解析。解决要么改用 openpyxl 写 xlsx要么写 csv 时加 BOM。我现在的习惯是直接上 xlsx省得解释。5.2 现象PDF 里中文显示成方块或空白原因reportlab 没注册中文字体或注册的字体文件路径不对。解决确认字体文件存在Windows 用simhei.ttf或msyh.ttfLinux 服务器上装fonts-noto-cjk后用/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc。注册后所有Paragraph和Table都要显式指定fontName漏一个地方就出方块。5.3 现象金额合计对不上差几块钱原因金额字段用了浮点数累加时出现精度误差如 0.10.20.30000000000000004。解决金额在数据库里用整数存「分」显示时除以 100或者用decimal.Decimal。礼簿场景金额都是整数元我一般直接存整数元彻底避开浮点。5.4 现象切换事项后还能看到上一个事项的数据原因查询时忘了带event_id条件或者会话里的current_event_id没更新。解决所有涉及gifts表的查询强制带event_id在数据访问层做一层封装不允许裸写 SQL。这是权限隔离的底线漏一次就是数据泄露。5.5 现象多人合并后出现重复记录原因去重键选得太窄只按姓名去重同名不同金额的被误判。解决去重键用「姓名金额桌号」合并后生成一份「疑似重复」清单让人工复核。软件不做最终裁决只做提示。6. 进阶把礼簿数据用起来——回礼提醒与年度人情统计账本记完不是终点。红白喜事的人情是有来有往的今年收了张三 500明年张三家办事你得还回去。我后来加了一个「人情往来」视图把同一个人在所有事项里的送礼记录拉出来按时间排序还礼时心里有数。-- 某个人在所有事项中的送礼记录 SELECT e.name AS 事项, e.event_date AS 日期, g.amount AS 金额, g.remark FROM gifts g JOIN events e ON g.event_id e.id WHERE g.guest_name ? ORDER BY e.event_date DESC; -- 年度收支按事项类型汇总 SELECT e.event_type AS 类型, COUNT(DISTINCT e.id) AS 场次, SUM(g.amount) AS 总额 FROM gifts g JOIN events e ON g.event_id e.id WHERE g.gift_typecash AND strftime(%Y, e.event_date) ? GROUP BY e.event_type;第一个查询解决「这人我该还多少」第二个查询解决「今年人情往来整体是收是支」。对于一条龙服务队第二个查询还能按客户维度统计看哪类宴席办得多、平均礼金水平如何接单报价时有参考。验证导出是否正确的办法很简单导出后拿计算器随机抽 10 笔加一遍和软件里的合计对。我吃过亏有一次 PDF 导出时漏了最后一页的记录因为分页逻辑在数据量正好是每页行数整数倍时少渲染一页。后来每次导出都做一次「导出笔数 数据库笔数」的断言不通过就报警。def export_with_check(conn, event_id, path): db_count conn.execute( SELECT COUNT(*) FROM gifts WHERE event_id?, (event_id,)).fetchone()[0] export_pdf(conn, event_id, 事项, path) # 重新解析 PDF 统计行数简化用 pypdf 读页数估算 # 实际项目里我会在导出函数返回写入行数直接比对 assert db_count 0, 空事项不导出 return db_count这套东西做下来最大的体会是礼簿软件的技术难点不在功能多而在「现场不能出错」。收礼台一乱后面全是麻烦。所以宁可界面丑一点、操作步骤多一点也要把确认、去重、合计校验做扎实。密码和事项隔离是底线导出打印是交付物两者都不能省。希望帮到你。本文还有配套的精品资源点击获取