🟡 一般问题

以下问题影响用户体验或存在可改进空间。

P04 — category_id 未关联分类表,字段始终为 NULL

APIGET /api/transactions/:billId

说明

transactions 表有 category_id 字段(外键 → categories.id),但 CSV 导入和直接插入时该字段从未被赋值,始终为 NULL。

影响

  • 前端按分类筛选交易功能无法工作
  • 分类数据存在 categories 表中,但与 transactions 完全割裂

改进建议

CSV 导入时根据 category 列的值去 categories 表匹配(模糊或精确),填充 category_id

P05 — tx_year/tx_month 未生成,始终为 NULL

APIGET /api/transactions/:billId

说明

transactions 表有 tx_yeartx_month 字段,但导入时从未计算赋值。

影响

  • 按年月统计功能无法实现
  • 月度收支报表无法直接用 SQL 查询

改进建议

导入时从 tx_time 解析年份和月份。

P06 — api_key_encrypted 只用 Base64 编码,非真正加密

APIPUT /api/apikeys/user · GET /api/apikeys/user

说明

存储时直接 Buffer.from(apiKey).toString('base64'),任何人可直接解码。

影响

  • 数据库泄漏 = 所有 AI Key 泄漏
  • 团队成员可互相查看彼此的 Key 明文

改进建议

使用 crypto.createCipheriv + 环境变量密钥做 AES-256-GCM 加密,存储密文 + IV + authTag。

P07 — Anthropic Provider 在 /chat 不走 SSE 流

APIPOST /api/ai/chat

说明

前端 ChatBar 组件配置了 SSE 流式渲染,但 Anthropic provider 实际返回的是普通 JSON 响应(res.json()),不是 SSE 流。

影响

  • 使用 Anthropic Key 时,AI 回复不是逐字流出,而是等完全生成后一次性展示

改进建议

后端对 Anthropic 也实现 SSE 流(使用 ReadableStream + TextEncoder)。

P08 — 游客账号限制无提示,用户困惑

APIPOST /api/auth/guest-login

说明

同 IP 24h 内最多创建2个游客账号,超限后返回错误但无明确提示。

影响

  • 用户不知道限制规则,以为系统故障

改进建议

错误响应中明确说明限制规则("该 IP 已达游客数量上限,请注册正式账号")。

P09 — Refresh Token 过期无恢复手段

APIPOST /api/auth/refresh

说明

Refresh Token 过期后无法续期,必须重新登录。

影响

  • 用户30天不访问 = 强制重新登录

改进建议

延长 Refresh Token 有效期(如365天);或提供"记住我"选项;过期后通过邮箱验证码恢复会话。

P10 — 多端 Session 无主动管理

APIPOST /api/auth/logout

说明

当前 GET /api/auth/me 不返回在线设备列表,logout 接口只能按 refreshToken 删除单条。

影响

  • 用户无法查看当前在哪些设备登录
  • 无法远程注销其他设备

改进建议

增加 GET /api/auth/sessionsDELETE /api/auth/sessions/:tokenIdDELETE /api/auth/sessions/all 接口。

P11 — 跨账单导入 Key 时明文复制,无重加密

APIPOST /api/apikeys/import

说明

导入时直接复制 api_key_encrypted(Base64),由于 Base64 不是加密,可直接解码。

影响

  • 从 A 账单导入到 B 账单后,A 账单管理员仍可解码 B 账单的 Key

改进建议

导入时用目标账单所在服务器的加密密钥重新加密。

P12 — Key 过期时间字段存在但无管理 UI

APIapi_keys.expires_at(数据库字段,无对应 API/UI)

说明

api_keys 表有 expires_at 字段,但前端 DeveloperPage 无过期管理功能,PATCH /apikeys/user 也不接受 expires_at 参数。

影响

  • 用户不知道 Key 何时过期
  • 过期前无提醒,过期后 AI 功能静默失败

改进建议

PUT /api/apikeys/user 支持 expires_at 参数;DeveloperPage 显示 Key 过期倒计时。

P13 — custom_promptprompt 字段名不一致

APIPUT /api/bills/:id/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 — 成员列表暴露其他成员手机号

APIGET /api/bills/:id/members

说明

bill_members JOIN users 表时直接暴露了所有成员的 phonenicknameavatar_url。同一账单的任意成员(包括 viewer)都可以看到其他成员的完整手机号。

影响

  • 隐私泄漏:viewer 角色可见 owner/其他成员手机号
  • 不符合最小权限原则

改进建议

成员列表 API 默认隐藏手机号(脱敏展示,如 +86 138 **** 1234);或提供单独的"查看手机号"权限。

P15 — Whisper 仅支持 OpenAI,多 Provider 兜底失效

APIPOST /api/ai/transcribe

说明

Whisper 语音转写强制依赖 OpenAI Key,没有其他 Provider 的 fallback。当 OpenAI Key 不可用或配额耗尽时,语音输入功能完全无法使用。

影响

  • 使用 Anthropic/Gemini 作为主 AI 的用户无法使用语音输入

改进建议

支持第三方 ASR API(如 Google Speech-to-Text、Azure Speech),或 Whisper 不可用时给出明确提示。

P16 — 所有 Provider 失败时返回 502,无友好错误提示

APIPOST /api/ai/chat · POST /api/ai/voice-chat

说明

当所有配置的 AI Provider 都返回错误时,后端直接返回 HTTP 502,错误信息为 Provider 的原始错误文本。

影响

  • 用户收到"502 Bad Gateway"而非友好的"AI 服务暂时不可用"
  • 排查困难,不知道是 Key 过期、配额耗尽还是网络问题

改进建议

捕获所有 Provider 异常,返回统一的 503 或 { error: "AI服务暂时不可用,请检查Key配置" };区分 Key 无效(401)、配额耗尽(429)、网络超时。

P17 — AI 日志记录在 Provider 层,异常时不记录

APIPOST /api/ai/chat

说明

AI 调用日志(api_logs 表)在 Provider 成功返回后写入。如果 Provider 直接抛出异常(如网络超时、Key 无效),日志记录语句不会执行。

影响

  • 失败的 AI 请求不在 api_logs 中留下痕迹
  • Admin 后台 AI 日志查询只能看到成功记录,排查问题不完整

改进建议

在 try/catch 外层也记录失败日志(记录 error message、provider、model);或使用 finally 确保日志写入。

P18 — coupon 字段类型前端期望字符串,后端返回数字

APIGET /api/transactions/:billId

说明

前端 TransactionItem 定义 coupon 为 string | null,后端实际返回数字类型(number | null)。

影响

  • 前端类型检查失败,coupon 字段可能显示异常

改进建议

统一 coupon 字段类型,建议后端返回字符串(与 CSV 中的原始值一致)。

P19 — CSV 解析时部分列从未被使用

APIPOST /api/transactions/:billId/upload

说明

CSV 列映射中,category 列虽被解析但未用于关联 categories 表(category_id 始终为 NULL,见 P04)。coupon 列也被读取但未写入 coupon 字段。

影响

  • 用户填写的分类信息和优惠券信息被忽略
  • 字段功能形同虚设

改进建议

移除未使用的 CSV 列解析代码,或补全 category_idcoupon 的写入逻辑。