简介:这是一款面向红白喜事办宴场景的电子礼簿记账工具,适合需要操办婚礼、满月宴、寿宴、白事等事务的个人或帮亲友打理礼金的人员使用。它支持创建多个独立事项并分别设置管理密码,记录姓名、金额、支付方式与备注,自动统计总人数和总金额,还提供搜索、筛选、排序、语音播报、内联修改留痕、分批打印、Excel导入导出、加密备份恢复及统计分析等功能,并支持竖排礼单展示与字体、颜色、背景自定义,可导出Excel或PDF打印。资源包共2000个文件,以js、md、json为主,另含少量py、html、xml、yaml等,压缩包约609.58MB,属于完整的Electron桌面应用工程。目前已有470人学习下载,适合想研究桌面端记账工具实现或直接部署使用的读者参考。
1. 电子礼簿 3.0:多事项分账与独立密码,为什么值得自己搭一套
村里办一场婚礼,收礼台三个人轮班,一本红皮礼簿从早写到晚,散场后对账发现有三笔金额对不上、两个名字写重了。这种场景做过红白喜事记账的人都懂——纸质礼簿最大的问题不是写不下,而是没法查、没法改、没法分。电子礼簿 3.0 这类工具要解决的核心就三件事:把每一笔礼金结构化存下来、按事项分开管理、导出成 PDF 或 Excel 能直接打印或存档。标题里提到的「支持创建多个事项」和「可单独设置管理密码」,恰恰是从「一个本子记一场事」升级到「一个软件管所有事」的关键分水岭。婚礼、满月宴、乔迁、寿宴、白事,每场事的宾客名单和金额逻辑完全不同,混在一起就是一笔糊涂账。这套方案适合经常帮人操办红白喜事的管事人、村里负责记账的文书、以及想给自家办事留个干净台账的普通人。下面从数据模型讲到导出打印,把能复现的路径拆开。
2. 多事项数据模型:一张表怎么装下婚礼和满月宴
2.1 事项与礼金记录的分层设计
电子礼簿最容易翻车的地方,是把所有礼金塞进一张表,靠一个「事项名称」字段区分。刚开始能用,等到你要统计「张三在婚礼和满月宴各随了多少」时,查询就变成一团乱麻。常见做法是分两张表:事项表(events)和礼金记录表(gift_records),用外键关联。事项表存事项名称、日期、地点、管理密码哈希;礼金记录表存姓名、金额、关系、备注、所属事项 ID。
-- 事项表:每场红白喜事一条记录 CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 事项名称,如"2024年10月婚礼" event_date TEXT NOT NULL, -- 日期,ISO 格式 2024-10-01 location TEXT, -- 地点,可空 pwd_hash TEXT, -- 管理密码的哈希值,可空表示不设密码 created_at TEXT DEFAULT (datetime('now','localtime')) ); -- 礼金记录表:每笔礼金一条记录 CREATE TABLE gift_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER NOT NULL, -- 关联事项 guest_name TEXT NOT NULL, -- 宾客姓名 amount REAL NOT NULL DEFAULT 0, -- 金额,用 REAL 兼容小数 relation TEXT, -- 关系,如"同事""亲戚" note TEXT, -- 备注 created_at TEXT DEFAULT (datetime('now','localtime')), FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE ); -- 按事项查礼金时走这个索引,数据量上千条也不卡 CREATE INDEX idx_gift_event ON gift_records(event_id);这段建表语句的逻辑很直白:事项和礼金是一对多关系,删事项时级联删掉它的礼金记录,避免留下孤儿数据。pwd_hash存哈希而不是明文,这是底线——哪怕本地软件,也不该把密码原样写进数据库文件。amount用 REAL 而不是 INTEGER,因为有些地方随礼会带零头,比如 888.88。索引idx_gift_event是必须加的,导出某个事项的 PDF 时要全表扫描还是走索引,几百条以上差距很明显。
2.2 管理密码的存储与校验
「可单独设置管理密码」这个需求,落地时有两个坑:一是密码存明文,二是用简单哈希不加盐。正确做法是用带盐的哈希,Python 里直接用hashlib.pbkdf2_hmac或bcrypt。
import hashlib, os, sqlite3 def set_event_password(conn, event_id, plain_pwd): """给指定事项设置管理密码,存盐+哈希""" salt = os.urandom(16) # 每个事项独立盐值 dk = hashlib.pbkdf2_hmac('sha256', plain_pwd.encode(), salt, 100000) stored = salt.hex() + ':' + dk.hex() # 盐和哈希拼一起存 conn.execute("UPDATE events SET pwd_hash=? WHERE id=?", (stored, event_id)) conn.commit() def verify_event_password(conn, event_id, plain_pwd): """校验密码,返回 True/False""" row = conn.execute("SELECT pwd_hash FROM events WHERE id=?", (event_id,)).fetchone() if not row or not row[0]: return True # 没设密码视为通过 salt_hex, dk_hex = row[0].split(':') salt = bytes.fromhex(salt_hex) dk = hashlib.pbkdf2_hmac('sha256', plain_pwd.encode(), salt, 100000) return dk.hex() == dk_hex参数说明:pbkdf2_hmac的迭代次数设 100000 是常见平衡点,太低容易被暴力破解,太高在低配电脑上校验会卡顿。盐值必须每个事项独立生成,不能全局共用一个,否则一个事项密码泄露会牵连其他事项。校验时用==比较哈希值在本地单机场景够用,如果做网络服务要换成hmac.compare_digest防时序攻击。没设密码的事项直接返回 True,这样用户可以选择哪些事项需要保护、哪些不需要。
3. 从录入到导出:PDF 和 Excel 两条落地路径
3.1 用 Python 生成可打印的 PDF 礼簿
导出 PDF 是电子礼簿最刚需的功能,因为红白喜事现场经常要打印出来贴在墙上或存档。Python 生态里生成 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 import sqlite3 # 注册中文字体,Windows 下用系统自带宋体,Linux 换成对应路径 pdfmetrics.registerFont(TTFont('SimSun', 'C:/Windows/Fonts/simsun.ttc')) def export_event_pdf(event_id, out_path): conn = sqlite3.connect('giftbook.db') event = conn.execute("SELECT name, event_date FROM events WHERE id=?", (event_id,)).fetchone() rows = conn.execute( "SELECT guest_name, amount, relation, note FROM gift_records WHERE event_id=? ORDER BY id", (event_id,) ).fetchall() doc = SimpleDocTemplate(out_path, pagesize=A4) style = ParagraphStyle('cn', fontName='SimSun', fontSize=12, leading=18) data = [['序号', '姓名', '金额', '关系', '备注']] total = 0 for i, r in enumerate(rows, 1): data.append([str(i), r[0], f'{r[1]:.2f}', r[2] or '', r[3] or '']) total += r[1] data.append(['', '合计', f'{total:.2f}', '', '']) table = Table(data, colWidths=[50, 100, 80, 80, 120]) table.setStyle(TableStyle([ ('FONTNAME', (0,0), (-1,-1), 'SimSun'), ('GRID', (0,0), (-1,-1), 0.5, colors.grey), ('BACKGROUND', (0,0), (-1,0), colors.lightgrey), ('ALIGN', (2,1), (2,-1), 'RIGHT'), ])) doc.build([Paragraph(f'{event[0]} 礼金明细({event[1]})', style), table]) conn.close()逻辑说明:先注册中文字体,simsun.ttc是 Windows 宋体路径,Linux 服务器上要换成/usr/share/fonts/...下的实际字体文件。表格第一行是表头,最后一行追加合计,金额用:.2f保留两位小数。colWidths控制各列宽度,A4 纸总宽约 523 点,五列加起来别超。TableStyle里GRID画网格线,BACKGROUND给表头加灰底,打印出来层次清楚。如果事项记录超过一页,SimpleDocTemplate会自动分页,但表头不会重复——要重复表头得用LongTable并设repeatRows=1。
3.2 导出 Excel 并处理打印区域
Excel 导出的价值在于用户可以自己二次编辑、筛选、做透视表。用openpyxl写 xlsx,顺手把打印区域和列宽设好,省得用户拿到文件还要手动调。
from openpyxl import Workbook from openpyxl.styles import Font, Alignment, Border, Side import sqlite3 def export_event_excel(event_id, out_path): conn = sqlite3.connect('giftbook.db') event = conn.execute("SELECT name FROM events WHERE id=?", (event_id,)).fetchone() rows = conn.execute( "SELECT guest_name, amount, relation, note FROM gift_records WHERE event_id=? ORDER BY id", (event_id,) ).fetchall() wb = Workbook() ws = wb.active ws.title = event[0][:30] # 工作表名最长 31 字符 headers = ['序号', '姓名', '金额', '关系', '备注'] ws.append(headers) for i, r in enumerate(rows, 1): ws.append([i, r[0], r[1], r[2] or '', r[3] or '']) # 表头加粗居中,设列宽 for col, width in zip('ABCDE', [6, 14, 10, 12, 20]): ws.column_dimensions[col].width = width for cell in ws[1]: cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center') # 设置打印区域和横向打印,避免列被截断 ws.print_area = f'A1:E{ws.max_row}' ws.page_setup.orientation = 'landscape' ws.page_setup.fitToWidth = 1 wb.save(out_path) conn.close()参数说明:ws.title截断到 30 字符是因为 Excel 工作表名上限 31 字符,超了会报错。print_area限定打印范围,fitToWidth=1让所有列缩到一页宽,横向打印适合五列以上的表。金额列没做数字格式,如果想让 Excel 里显示成货币,加cell.number_format = '#,##0.00'。导出后用户如果发现 Ctrl+V 用不了,通常是文件被其他进程占用或剪贴板被输入法劫持,跟导出逻辑无关,重启 Excel 即可。
4. 避坑与排查:多事项记账最容易翻车的 5 个地方
4.1 现象:切换事项后录入的礼金跑到了别的事项下
原因:前端或界面层没有把当前选中的event_id正确传给写入函数,或者用了全局变量存事项 ID,切换时没更新。解决:每次写入前从当前界面状态重新读取event_id,不要依赖缓存变量;写入后立刻用SELECT event_id FROM gift_records WHERE id=last_insert_rowid()回查确认。
4.2 现象:PDF 导出中文全是方块或乱码
原因:reportlab默认字体 Helvetica 不含中文字形,注册字体时路径写错或字体文件本身是 ttc 集合未指定索引。解决:确认字体文件存在,Windows 用simsun.ttc时TTFont能自动取第一个字体;Linux 上建议用NotoSansCJK-Regular.ttc并显式传subfontIndex=0。导出前先跑一个只写「测试中文」的最小 PDF 验证字体。
4.3 现象:Excel 导出后金额列变成文本,无法求和
原因:写入时金额被转成了字符串,或者单元格格式被设成文本。解决:ws.append时直接传 float,不要str(r[1]);如果已经写成文本,用cell.value = float(cell.value)批量转换。导出后按 Ctrl+End 定位到最后一个单元格,确认金额列右对齐(数字默认右对齐,文本左对齐)。
4.4 现象:设了管理密码的事项,忘记密码后数据打不开
原因:密码哈希不可逆,没有留后门。解决:这是设计上的取舍,不是 bug。常见做法是在设密码时强制用户填一个「密码提示」字段存明文,或者提供一个导出加密备份的功能,用另一套恢复码解密。我一般会建议用户把密码记在事项备注里,但备注本身不加密——所以更稳妥的是导出时同时生成一份不含密码的只读 PDF 存档。
4.5 现象:礼金记录上千条后,导出 PDF 卡死或内存暴涨
原因:一次性把所有记录读进内存再构建表格,reportlab的 Table 对超大列表渲染效率低。解决:分页查询,每 200 条构建一个 Table 追加到story列表;或者改用LongTable配合repeatRows=1。SQLite 查询时加LIMIT/OFFSET,避免fetchall()一次拉全量。实测 5000 条记录分页导出比一次性导出快 3 倍以上。
5. 进阶技巧:用模板批量导出多事项汇总与打印校验
单事项导出跑通后,真正省时间的是批量操作。比如年底要把全年五场红白喜事的礼金汇总成一份总表,或者给每个事项生成独立 PDF 后合并打印。这里给一个批量导出加打印校验的脚本骨架。
import os, sqlite3 from export_pdf import export_event_pdf # 复用前面的导出函数 def batch_export_all(db_path, out_dir): """遍历所有事项,逐个导出 PDF,并生成汇总 CSV""" os.makedirs(out_dir, exist_ok=True) conn = sqlite3.connect(db_path) events = conn.execute("SELECT id, name FROM events ORDER BY event_date").fetchall() summary = [] for eid, name in events: safe_name = ''.join(c for c in name if c not in r'\/:*?"<>|') pdf_path = os.path.join(out_dir, f'{safe_name}.pdf') export_event_pdf(eid, pdf_path) cnt, total = conn.execute( "SELECT COUNT(*), COALESCE(SUM(amount),0) FROM gift_records WHERE event_id=?", (eid,) ).fetchone() summary.append((name, cnt, total)) print(f'[OK] {name}: {cnt} 笔, 合计 {total:.2f} -> {pdf_path}') # 写汇总 CSV,方便直接看 with open(os.path.join(out_dir, '汇总.csv'), 'w', encoding='utf-8-sig') as f: f.write('事项,笔数,合计金额\n') for name, cnt, total in summary: f.write(f'{name},{cnt},{total:.2f}\n') conn.close() if __name__ == '__main__': batch_export_all('giftbook.db', './exports')这段脚本的关键点:文件名过滤掉 Windows 不允许的字符\/:*?"<>|,否则事项名里带斜杠会直接报错。CSV 用utf-8-sig编码,Excel 打开不会乱码。汇总统计用COALESCE(SUM(amount),0)处理空事项,避免返回 None。
打印校验方面,导出 PDF 后建议用pdfinfo(poppler 工具)或 Python 的PyPDF2检查页数和文件大小,确认没有生成 0 页的空文件。批量导出 20 个事项后,我会习惯性打开exports目录按大小排序,最小的那个文件往往就是出问题的——要么事项没记录,要么字体没注册成功。这个习惯帮我省过好几次返工。
另外,如果用户反馈「打印出来表格超出纸张」,优先检查colWidths总和是否超过 A4 可用宽度(约 523 点),以及 Excel 的fitToWidth是否设为 1。打印预览里看一眼比什么都快。做这类工具,数据准确性是底线,导出只是最后一公里,但往往就是这一公里让用户觉得「能用」还是「好用」。希望帮到你。
本文还有配套的精品资源,点击获取