feat: 邮箱找回密码 + 日志体系完善 + SMTP 配置补全
## 功能 - 邮箱找回密码:新增 password_resets 表 + POST /api/auth/forgot-password (按接收邮箱反查用户 → 生成一次性 30 分钟令牌 → 复用 SMTP 发重置邮件, 防枚举统一提示、复用登录限流器防刷)+ POST /api/auth/reset (校验令牌有效性 → bcrypt 更新密码 → 作废令牌 → 强制该用户全部会话下线); H5 新增 /forgot、/reset 页面,登录页加"忘记密码"入口 - SMTP 全局配置补全:新增发件邮箱 smtp_user / 授权码 smtp_pass 字段 (H5 + 小程序设置页同步展示与保存),GET 脱敏回显, 修复"脱敏值含 * 被 POST 写回覆盖真实配置"的 bug - 新增 cmd/sendmail 真实发送工具(配置走环境变量,敏感信息不落库, 已用 163 SMTP 465 隐式 SSL 实测发送成功) ## 日志体系 - 死配置 OIL_MONITOR_LOG_DIR 替换为 OIL_MONITOR_LOG_LEVEL (debug/info/warn/error,LevelVar 动态切换,上线默认 info 收敛日志) - 自定义 requestLogger 请求日志中间件(Debug 级:method/path/status/耗时/IP) - 外部交互日志统一:爬虫抓取 / SMTP / Webhook / 高德 regeo / 微信 code2session, 成功 Debug、失败 Error、均带 dur_ms、URL 去 query 脱敏防 key/token 泄露 - 定时任务统一 runTask 包装:开始 Debug、完成+时长 Info、失败/panic Error (panic 被 recover,不连累调度器) ## 测试 - cron/scheduler_test.go:runTask 成功/失败/panic 三路径单测 - push/sender_test.go:本地假 SMTP 服务器完整发送会话链路 + 服务器拒绝失败路径 - db_test.go:表数量断言 11 → 12(新增 password_resets) ## 部署配置与文档 - Dockerfile / docker-compose.yml / .env.example / README:日志配置同步 (删 /app/logs 目录与挂载,OIL_MONITOR_LOG_LEVEL) - .gitignore:忽略 internal/pkg 依赖缓存(349M,防误提交) - docs/10/21/22/23/24:同步 A18/A19 接口、password_resets 表、任务执行日志、SMTP 字段 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
2f13706e54
commit
d6d034727b
34 files changed
+938
-70
No files matched your search
+1
-1
@@ -261,7 +261,7 @@
|
||||
|
||||
| 表 | 内容 | 权限 |
|
||||
|---|---|---|
|
||||
| `server_settings`(全局) | `amap_key`、`smtp_host`、`smtp_port`、全局`threshold_per_liter`、全局`default_province`、采集状态 `last_crawl_*` | 仅 admin 可写 |
|
||||
| `server_settings`(全局) | `amap_key`、`smtp_host`/`smtp_port`/`smtp_user`/`smtp_pass`、全局`threshold_per_liter`、全局`default_province`、采集状态 `last_crawl_*` | 仅 admin 可写 |
|
||||
| `user_settings`(个人) | 飞书/企微/钉钉 `webhook`、`receiver_email`、个人`threshold_per_liter`、个人`default_province` | 按 `user_id` 归属 |
|
||||
|
||||
---
|
||||
|
||||
+7
-1
@@ -199,7 +199,7 @@ function pushRecent(province):
|
||||
005.3 ▲前端 若当前用户是 admin(/api/auth/me 返回 is_admin=true)→ 额外加载"全局设置区"
|
||||
├─ 调用 GET /api/settings/server ⟶ [A10] → 渲染:
|
||||
│ ├─ 高德 Key(amap_key)
|
||||
│ └─ SMTP 主机/端口(smtp_host / smtp_port)
|
||||
│ └─ SMTP 主机/端口/发件邮箱/授权码(smtp_host / smtp_port / smtp_user / smtp_pass)
|
||||
└─ 005.4 ▲前端 保存全局设置 → POST /api/settings/server ⟶ [A10]
|
||||
005.5 ▲前端 点击"发送测试消息"(选择渠道)→ POST /api/settings/test ⟶ [A11] → 展示发送结果
|
||||
005.6 ▲前端 切换"自动定位开关" → 写入 localStorage(auto_location),关闭时首页跳过 001.2 定位模块
|
||||
@@ -408,6 +408,12 @@ function pushRecent(province):
|
||||
014.3 ▲前端 回跳 → 重新执行触发原流程(如进入 003)
|
||||
014.4 ▲前端 用户点击"退出登录" → POST /api/auth/logout ⟶ [A16] → 清 Cookie → 回首页
|
||||
014.5 ▲前端 应用启动时 GET /api/auth/me ⟶ [A17] 探登录态(对应 001.1.3)
|
||||
014.6 ▲前端 忘记密码(登录页链接 → /forgot 输接收邮箱)→ POST /api/auth/forgot-password ⟶ [A18]
|
||||
├─ 成功 → 提示"重置邮件已发送"(无论邮箱是否存在均同文案,防枚举)
|
||||
└─ 邮件内含 /reset?token= 链接(30 分钟有效)
|
||||
014.7 ▲前端 打开邮件链接 → 输新密码两次 → POST /api/auth/reset ⟶ [A19]
|
||||
├─ 成功 → 密码已重置并强制下线全部会话 → 跳登录页
|
||||
└─ 失败(令牌无效/已用/过期)→ 提示重新申请
|
||||
```
|
||||
|
||||
**出口**:会话建立(Cookie)或注销;回跳至触发点。
|
||||
+108
-3
@@ -22,6 +22,8 @@
|
||||
| `GET /api/location/regeo` | ✅ | ✅ | ✅ |
|
||||
| `GET /api/health` | ✅ | ✅ | ✅ |
|
||||
| `POST /api/auth/register` `login` | ✅ | — | — |
|
||||
| `POST /api/auth/forgot-password` | ✅ | — | — |
|
||||
| `POST /api/auth/reset` | ✅ | — | — |
|
||||
| `GET /api/auth/me` | ✅(返回未登录态) | ✅ | ✅ |
|
||||
| `POST /api/auth/logout` | — | ✅ | ✅ |
|
||||
| `GET/POST /api/vehicles*` | ❌ 401 | ✅ 按 user_id | ✅ |
|
||||
@@ -801,19 +803,23 @@ ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value;
|
||||
"settings": {
|
||||
"amap_key": "****8765",
|
||||
"smtp_host": "smtp.qq.com",
|
||||
"smtp_port": 465
|
||||
"smtp_port": 465,
|
||||
"smtp_user": "****@qq.com",
|
||||
"smtp_pass": "****abcd"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**POST 请求体**(部分更新,空串清空)
|
||||
**POST 请求体**(部分更新,空串清空;脱敏值不写回)
|
||||
|
||||
```json
|
||||
{
|
||||
"amap_key": "abcdef1234567890",
|
||||
"smtp_host": "smtp.qq.com",
|
||||
"smtp_port": 465
|
||||
"smtp_port": 465,
|
||||
"smtp_user": "oil-monitor@qq.com",
|
||||
"smtp_pass": "授权码"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -825,6 +831,8 @@ ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value;
|
||||
| amap_key | body | string | 否 | 高德 Web 服务 Key(16/32 位);空串清空 |
|
||||
| smtp_host | body | string | 否 | SMTP 服务器地址;空串清空 |
|
||||
| smtp_port | body | int | 否 | SMTP 端口,(0, 65535] |
|
||||
| smtp_user | body | string | 否 | SMTP 发件邮箱账号(邮件 From 与登录账号);空串清空 |
|
||||
| smtp_pass | body | string | 否 | SMTP 授权码(非邮箱登录密码);空串清空 |
|
||||
|
||||
**响应字段说明**
|
||||
|
||||
@@ -837,6 +845,8 @@ ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value;
|
||||
| data.settings.amap_key | string | 高德 Key(脱敏,仅留后 4 位) |
|
||||
| data.settings.smtp_host | string | SMTP 服务器地址 |
|
||||
| data.settings.smtp_port | int | SMTP 端口 |
|
||||
| data.settings.smtp_user | string | SMTP 发件邮箱(脱敏,仅留后 4 位) |
|
||||
| data.settings.smtp_pass | string | SMTP 授权码(脱敏,仅留后 4 位) |
|
||||
|
||||
**校验**:`smtp_port` ∈ (0, 65535];`amap_key` 非空时建议为高德 16/32 位 Web 服务 Key 格式。
|
||||
|
||||
@@ -1244,3 +1254,98 @@ SELECT u.id, u.username, u.is_admin
|
||||
FROM sessions s JOIN users u ON s.user_id = u.id
|
||||
WHERE s.token = ? AND s.expires_at > datetime('now','localtime');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## A18 · POST /api/auth/forgot-password(🔓匿名)
|
||||
|
||||
**功能**:邮箱找回第一步——按「接收邮箱」反查用户,生成一次性重置令牌并通过全局 SMTP 配置发送重置邮件。无论邮箱是否存在均返回统一成功提示(防枚举)。
|
||||
|
||||
**请求**:`POST /api/auth/forgot-password`(🔓匿名)
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{ "email": "user@example.com" }
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| email | body | string | 是 | 用户在设置页填写的「接收邮箱」 |
|
||||
|
||||
**响应**(HTTP 200,统一提示防枚举)
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "ok", "data": { "message": "若该邮箱已注册,重置邮件已发送" } }
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
| 场景 | HTTP | code | message |
|
||||
|---|---|---|---|
|
||||
| 邮箱格式不正确 | 400 | 1002 | 邮箱格式不正确 |
|
||||
| 同 IP+邮箱 15 分钟超过 5 次 | 429 | 1014 | 尝试过于频繁,请稍后再试 |
|
||||
| SMTP 未配置 / 发送失败 | 500 | 1005 | 重置邮件发送失败:请确认已配置 SMTP 服务 |
|
||||
|
||||
**校验**:邮箱需含 `@`;限流复用登录限流器,键为 `fp:`+邮箱(与登录键互不冲突)。
|
||||
|
||||
**处理逻辑**:解析 email(转小写去空格)→ 限流校验 → `user_settings` 按 `receiver_email` 反查 `user_id`(一个邮箱可能对应多个账号)→ 对每个匹配用户:生成 32 字节随机 token 写 `password_resets`(30 分钟过期)→ 复用 `push.Send(smtp)` 发送含重置链接的邮件(链接域名取 `X-Forwarded-Proto` / `Host`)→ 无匹配用户也返回统一成功提示。
|
||||
|
||||
**DB 操作(SQL)**
|
||||
|
||||
```sql
|
||||
-- 按邮箱反查用户
|
||||
SELECT user_id FROM user_settings WHERE key = 'receiver_email' AND lower(value) = ?;
|
||||
-- 写入重置令牌
|
||||
INSERT INTO password_resets(token, user_id, expires_at, used, created_at)
|
||||
VALUES(?, ?, datetime('now','localtime','+30 minutes'), 0, datetime('now','localtime'));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## A19 · POST /api/auth/reset(🔓匿名)
|
||||
|
||||
**功能**:邮箱找回第二步——校验重置令牌(未过期、未使用)后更新密码,作废令牌并强制该用户所有会话下线。
|
||||
|
||||
**请求**:`POST /api/auth/reset`(🔓匿名)
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{ "token": "32字节hex", "password": "newpass123" }
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| token | body | string | 是 | 邮件重置链接中的令牌 |
|
||||
| password | body | string | 是 | 新密码(≥8 位) |
|
||||
|
||||
**响应**(HTTP 200)
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "ok", "data": { "message": "密码已重置,请使用新密码登录" } }
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
| 场景 | HTTP | code | message |
|
||||
|---|---|---|---|
|
||||
| 令牌缺失 / 无效 | 400 | 1002 | 重置令牌缺失 / 重置链接无效 |
|
||||
| 令牌已使用 | 400 | 1002 | 该重置链接已使用,请重新申请 |
|
||||
| 令牌已过期 | 400 | 1002 | 重置链接已过期,请重新申请 |
|
||||
| 密码不足 8 位 | 400 | 1002 | 密码至少 8 位 |
|
||||
|
||||
**处理逻辑**:校验 token 存在且 `used=0` 且 `expires_at` 未过 → `auth.HashPassword` 生成 bcrypt 哈希更新 `users.password_hash` → 标记令牌 `used=1` → 删除该用户全部 `sessions`(强制下线)→ 返回成功。
|
||||
|
||||
**DB 操作(SQL)**
|
||||
|
||||
```sql
|
||||
SELECT user_id, used, expires_at FROM password_resets WHERE token = ?;
|
||||
UPDATE users SET password_hash = ? WHERE id = ?;
|
||||
UPDATE password_resets SET used = 1 WHERE token = ?;
|
||||
DELETE FROM sessions WHERE user_id = ?;
|
||||
```
|
||||
@@ -42,6 +42,12 @@ ON CONFLICT(quote_date) DO UPDATE SET
|
||||
- 解析失败(页面结构变化):日志 error + 保留原始响应到 `logs/raw/` 便于排查。
|
||||
- 周末/节假日:新浪接口仍返回最近交易日数据,以响应日期为准,不产生脏数据。
|
||||
|
||||
**任务执行日志**:所有定时任务经统一包装器 `runTask` 注册(`internal/cron/scheduler.go`),记录执行生命周期——
|
||||
- 开始:`level=DEBUG msg=定时任务开始 task=<任务名>`(上线 `info` 静默)
|
||||
- 正常结束:`level=INFO msg=定时任务完成 task=<任务名> dur_ms=<耗时>`
|
||||
- 业务失败:`level=ERROR msg=定时任务失败 task=<任务名> dur_ms=<耗时> err=<原因>`
|
||||
- 异常中断:`level=ERROR msg=定时任务异常 task=<任务名> dur_ms=<耗时> panic=<内容>`(panic 被 recover,不连累调度器)
|
||||
|
||||
**衔接**:成功后 → `T4`;写 `server_settings.last_crawl_crude`。
|
||||
|
||||
---
|
||||
@@ -275,7 +281,7 @@ VALUES (?, ?, 'announce', ?, ?, ?, ?, datetime('now','localtime'));
|
||||
```
|
||||
1. 读渠道配置:
|
||||
- 飞书/企微/钉钉 webhook:来自 user_settings(user_id 指定)或 server_settings(匿名/系统告警用兜底)
|
||||
- SMTP 服务器:smtp_host/port 来自 server_settings(全局);收件人 receiver_email 来自 user_settings
|
||||
- SMTP 服务器:smtp_host/port/user/pass 来自 server_settings(全局);收件人 receiver_email 来自 user_settings
|
||||
2. 渠道未配置 → 返回 error("channel_not_configured")
|
||||
3. 发送:
|
||||
- Webhook 类:POST {url},Body:
|
||||
|
||||
+15
-2
@@ -29,6 +29,19 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
CREATE INDEX IF NOT EXISTS idx_sess_user ON sessions(user_id);
|
||||
```
|
||||
|
||||
**password_resets**(密码重置令牌,邮箱找回)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS password_resets (
|
||||
token TEXT PRIMARY KEY, -- 一次性重置令牌(crypto/rand 32 字节 hex)
|
||||
user_id INTEGER NOT NULL REFERENCES users(id), -- 归属用户 id
|
||||
expires_at TEXT NOT NULL, -- 过期时间(默认 +30 分钟)
|
||||
used INTEGER NOT NULL DEFAULT 0, -- 是否已使用:1=是
|
||||
created_at TEXT DEFAULT (datetime('now','localtime')) -- 创建时间
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pr_user ON password_resets(user_id);
|
||||
```
|
||||
|
||||
**price_adjustments**(调价事件)
|
||||
|
||||
```sql
|
||||
@@ -113,7 +126,7 @@ CREATE INDEX IF NOT EXISTS idx_pred_target ON predictions(target_date);
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS server_settings (
|
||||
key TEXT PRIMARY KEY, -- 配置键(如 amap_key / smtp_host / smtp_port / threshold_per_liter / default_province / last_crawl_*)
|
||||
key TEXT PRIMARY KEY, -- 配置键(如 amap_key / smtp_host / smtp_port / smtp_user / smtp_pass / threshold_per_liter / default_province / last_crawl_*)
|
||||
value TEXT NOT NULL -- 配置值
|
||||
);
|
||||
-- 预置默认值:
|
||||
@@ -140,7 +153,7 @@ CREATE TABLE IF NOT EXISTS notify_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 日志 id
|
||||
user_id INTEGER, -- 接收用户 id(系统告警可为 NULL)
|
||||
channel TEXT NOT NULL, -- 推送渠道:feishu/wecom/dingtalk/smtp
|
||||
notify_type TEXT NOT NULL, -- 通知类型:test/pre_alert/announce/warn
|
||||
notify_type TEXT NOT NULL, -- 通知类型:test/pre_alert/announce/warn/forgot
|
||||
title TEXT, -- 通知标题
|
||||
content TEXT, -- 通知内容
|
||||
status TEXT NOT NULL, -- 发送状态:sent/failed/skipped
|
||||
|
||||
Reference in new issue
Block a user