🟡 一般问题
以下问题影响用户体验或存在可改进空间。
P04 — category_id 未关联分类表,字段始终为 NULL
说明
transactions 表有 category_id 字段(外键 → categories.id),但 CSV 导入和直接插入时该字段从未被赋值,始终为 NULL。
影响
- 前端按分类筛选交易功能无法工作
- 分类数据存在
categories表中,但与transactions完全割裂
改进建议
CSV 导入时根据 category 列的值去 categories 表匹配(模糊或精确),填充 category_id。
P05 — tx_year/tx_month 未生成,始终为 NULL
说明
transactions 表有 tx_year、tx_month 字段,但导入时从未计算赋值。
影响
- 按年月统计功能无法实现
- 月度收支报表无法直接用 SQL 查询
改进建议
导入时从 tx_time 解析年份和月份。
P06 — api_key_encrypted 只用 Base64 编码,非真正加密
说明
存储时直接 Buffer.from(apiKey).toString('base64'),任何人可直接解码。
影响
- 数据库泄漏 = 所有 AI Key 泄漏
- 团队成员可互相查看彼此的 Key 明文
改进建议
使用 crypto.createCipheriv + 环境变量密钥做 AES-256-GCM 加密,存储密文 + IV + authTag。
P07 — Anthropic Provider 在 /chat 不走 SSE 流
说明
前端 ChatBar 组件配置了 SSE 流式渲染,但 Anthropic provider 实际返回的是普通 JSON 响应(res.json()),不是 SSE 流。
影响
- 使用 Anthropic Key 时,AI 回复不是逐字流出,而是等完全生成后一次性展示
改进建议
后端对 Anthropic 也实现 SSE 流(使用 ReadableStream + TextEncoder)。
P08 — 游客账号限制无提示,用户困惑
说明
同 IP 24h 内最多创建2个游客账号,超限后返回错误但无明确提示。
影响
- 用户不知道限制规则,以为系统故障
改进建议
错误响应中明确说明限制规则("该 IP 已达游客数量上限,请注册正式账号")。
P09 — Refresh Token 过期无恢复手段
说明
Refresh Token 过期后无法续期,必须重新登录。
影响
- 用户30天不访问 = 强制重新登录
改进建议
延长 Refresh Token 有效期(如365天);或提供"记住我"选项;过期后通过邮箱验证码恢复会话。
P10 — 多端 Session 无主动管理
说明
当前 GET /api/auth/me 不返回在线设备列表,logout 接口只能按 refreshToken 删除单条。
影响
- 用户无法查看当前在哪些设备登录
- 无法远程注销其他设备
改进建议
增加 GET /api/auth/sessions、DELETE /api/auth/sessions/:tokenId、DELETE /api/auth/sessions/all 接口。
P11 — 跨账单导入 Key 时明文复制,无重加密
说明
导入时直接复制 api_key_encrypted(Base64),由于 Base64 不是加密,可直接解码。
影响
- 从 A 账单导入到 B 账单后,A 账单管理员仍可解码 B 账单的 Key
改进建议
导入时用目标账单所在服务器的加密密钥重新加密。
P12 — Key 过期时间字段存在但无管理 UI
说明
api_keys 表有 expires_at 字段,但前端 DeveloperPage 无过期管理功能,PATCH /apikeys/user 也不接受 expires_at 参数。
影响
- 用户不知道 Key 何时过期
- 过期前无提醒,过期后 AI 功能静默失败
改进建议
PUT /api/apikeys/user 支持 expires_at 参数;DeveloperPage 显示 Key 过期倒计时。
P13 — custom_prompt 与 prompt 字段名不一致
说明
后端期望请求体字段名为 custom_prompt,但前端 DataContext.jsx 实际发送的是 prompt:
// DataContext.jsx 第235行
await api.put(`/bills/${activeBill.id}/prompt`, { prompt }); // ← 发送的是 prompt
// 后端期望
{ custom_prompt: "..." } // ← 后端读的是 custom_prompt
影响
- AI Custom Prompt 设置功能完全失效
改进建议
统一字段名,推荐以后端为准(custom_prompt)修改前端代码。
P14 — 成员列表暴露其他成员手机号
说明
bill_members JOIN users 表时直接暴露了所有成员的 phone、nickname、avatar_url。同一账单的任意成员(包括 viewer)都可以看到其他成员的完整手机号。
影响
- 隐私泄漏:viewer 角色可见 owner/其他成员手机号
- 不符合最小权限原则
改进建议
成员列表 API 默认隐藏手机号(脱敏展示,如 +86 138 **** 1234);或提供单独的"查看手机号"权限。
P15 — Whisper 仅支持 OpenAI,多 Provider 兜底失效
说明
Whisper 语音转写强制依赖 OpenAI Key,没有其他 Provider 的 fallback。当 OpenAI Key 不可用或配额耗尽时,语音输入功能完全无法使用。
影响
- 使用 Anthropic/Gemini 作为主 AI 的用户无法使用语音输入
改进建议
支持第三方 ASR API(如 Google Speech-to-Text、Azure Speech),或 Whisper 不可用时给出明确提示。
P16 — 所有 Provider 失败时返回 502,无友好错误提示
说明
当所有配置的 AI Provider 都返回错误时,后端直接返回 HTTP 502,错误信息为 Provider 的原始错误文本。
影响
- 用户收到"502 Bad Gateway"而非友好的"AI 服务暂时不可用"
- 排查困难,不知道是 Key 过期、配额耗尽还是网络问题
改进建议
捕获所有 Provider 异常,返回统一的 503 或 { error: "AI服务暂时不可用,请检查Key配置" };区分 Key 无效(401)、配额耗尽(429)、网络超时。
P17 — AI 日志记录在 Provider 层,异常时不记录
说明
AI 调用日志(api_logs 表)在 Provider 成功返回后写入。如果 Provider 直接抛出异常(如网络超时、Key 无效),日志记录语句不会执行。
影响
- 失败的 AI 请求不在
api_logs中留下痕迹 - Admin 后台 AI 日志查询只能看到成功记录,排查问题不完整
改进建议
在 try/catch 外层也记录失败日志(记录 error message、provider、model);或使用 finally 确保日志写入。
P18 — coupon 字段类型前端期望字符串,后端返回数字
说明
前端 TransactionItem 定义 coupon 为 string | null,后端实际返回数字类型(number | null)。
影响
- 前端类型检查失败,coupon 字段可能显示异常
改进建议
统一 coupon 字段类型,建议后端返回字符串(与 CSV 中的原始值一致)。
P19 — CSV 解析时部分列从未被使用
说明
CSV 列映射中,category 列虽被解析但未用于关联 categories 表(category_id 始终为 NULL,见 P04)。coupon 列也被读取但未写入 coupon 字段。
影响
- 用户填写的分类信息和优惠券信息被忽略
- 字段功能形同虚设
改进建议
移除未使用的 CSV 列解析代码,或补全 category_id 和 coupon 的写入逻辑。