953 lines
33 KiB
Markdown
953 lines
33 KiB
Markdown
# URL 地址池与会员注册域名绑定设计方案
|
||
|
||
> 文档状态:方案设计
|
||
> 方案版本:v2.1
|
||
> 最后更新:2026-10-15
|
||
> 适用项目:Olons Admin / HotGo
|
||
> 关联模块:会员注册、会员登录、会员管理、URL 地址池管理、会员登录日志
|
||
|
||
## 1. 方案变更说明
|
||
|
||
原方案采用“会员注册时从 URL 地址池轮询取出一个 URL”的模式。现调整为“会员通过哪个域名打开注册页面,就与哪个域名建立绑定关系”。
|
||
|
||
新流程如下:
|
||
|
||
```text
|
||
管理员或推广人员先向用户提供一个注册链接
|
||
→ 用户通过该链接打开注册页面
|
||
→ 注册页面获取浏览器地址栏中的域名
|
||
→ 用户提交注册
|
||
→ 后端校验本次请求的真实注册域名
|
||
→ 创建会员
|
||
→ 建立会员与注册域名的绑定关系
|
||
```
|
||
|
||
本方案不再使用 URL 轮询顺序、分配游标和自动轮询算法。URL 地址池的作用调整为:
|
||
|
||
1. 维护允许用于会员注册的域名;
|
||
2. 管理域名的启用、失效状态;
|
||
3. 统计每个域名带来的注册会员;
|
||
4. 查询会员与注册域名的绑定关系。
|
||
|
||
每个地址同时维护累计绑定次数。地址池列表默认把绑定次数较少的地址排在前面;点击地址详情可以查看当前绑定该地址的全部会员。
|
||
|
||
## 2. 设计目标
|
||
|
||
1. 用户使用某个域名注册时,自动与该域名绑定。
|
||
2. 注册域名由后端根据可信请求信息确认,不能只信任前端传入值。
|
||
3. 每个会员只能存在一个当前绑定域名。
|
||
4. 域名未配置或已失效时,会员仍允许注册成功,但保持“待绑定”。
|
||
5. 后台不提供创建会员、手动绑定或重新绑定域名的入口。
|
||
6. 域名失效后不再接受新的自动绑定,但保留原有绑定用于审计。
|
||
7. 支持同域部署、前后端跨域部署以及 CDN/反向代理部署。
|
||
8. 所有注册自动绑定和地址状态变更都有业务审计记录。
|
||
9. 每个地址记录累计绑定次数,地址池列表默认按绑定次数升序排列。
|
||
10. 后台可以手动将启用地址切换为失效,也可以将失效地址重新启用。
|
||
11. C 端会员登录成功后更新会员最后登录时间、IP脱敏信息和地区。
|
||
12. C 端会员每次登录成功或失败均记录独立日志,并可在后台查询、查看和导出。
|
||
|
||
## 3. 核心概念
|
||
|
||
### 3.1 注册 URL
|
||
|
||
管理员或推广人员提供给用户的完整注册链接,例如:
|
||
|
||
```text
|
||
https://register-a.example.com/register?inviteCode=ABC123
|
||
```
|
||
|
||
完整 URL 中可能包含路径、邀请码或其他查询参数,但会员绑定只取域名部分:
|
||
|
||
```text
|
||
register-a.example.com
|
||
```
|
||
|
||
默认端口不作为域名的一部分:
|
||
|
||
```text
|
||
https://register-a.example.com:443 → register-a.example.com
|
||
http://register-a.example.com:80 → register-a.example.com
|
||
```
|
||
|
||
非默认端口的测试环境可以保留端口:
|
||
|
||
```text
|
||
http://127.0.0.1:5173 → 127.0.0.1:5173
|
||
```
|
||
|
||
### 3.2 URL 地址池
|
||
|
||
URL 地址池是允许用于注册绑定的域名白名单。地址池中的域名分为:
|
||
|
||
| 值 | 状态 | 说明 |
|
||
|---:|---|---|
|
||
| 1 | 启用 | 允许新注册会员自动绑定 |
|
||
| 2 | 失效 | 不允许新注册会员自动绑定,保留原绑定关系 |
|
||
|
||
### 3.3 会员域名绑定
|
||
|
||
会员注册成功后,如果识别出的域名在地址池中存在且为启用状态,则创建绑定记录。
|
||
|
||
一个会员最多有一个绑定域名。绑定只允许在 C 端会员注册事务中自动建立,后台只能查看,不能手动绑定、解除绑定或重新绑定。
|
||
|
||
### 3.4 待绑定会员
|
||
|
||
以下情况会员注册成功但不建立域名绑定:
|
||
|
||
- 请求中无法识别有效注册域名;
|
||
- 注册域名没有配置在地址池中;
|
||
- 注册域名已经失效;
|
||
|
||
没有绑定记录的会员统一显示为“待绑定”。
|
||
|
||
## 4. 业务规则
|
||
|
||
### 4.1 正常注册绑定
|
||
|
||
```text
|
||
用户打开 https://register-a.example.com/register
|
||
→ 前端读取 window.location.origin
|
||
→ 用户提交注册
|
||
→ 后端确认真实来源域名为 register-a.example.com
|
||
→ 查询地址池
|
||
→ 地址存在且启用
|
||
→ 创建会员及钱包
|
||
→ 创建会员与 register-a.example.com 的绑定
|
||
→ 注册成功
|
||
```
|
||
|
||
### 4.2 地址池没有该域名
|
||
|
||
```text
|
||
用户通过 unknown.example.com 注册
|
||
→ 后端未在地址池找到该域名
|
||
→ 会员和钱包正常创建
|
||
→ 不创建域名绑定记录
|
||
→ 记录“未知注册域名”业务审计
|
||
→ 会员显示为待绑定
|
||
```
|
||
|
||
### 4.3 域名已经失效
|
||
|
||
```text
|
||
用户通过已失效域名注册
|
||
→ 会员正常创建
|
||
→ 不建立自动绑定
|
||
→ 记录“通过失效域名注册”业务审计
|
||
→ 会员显示为待绑定
|
||
```
|
||
|
||
### 4.4 重复注册请求
|
||
|
||
同一会员创建请求因网络原因重复提交时,应通过现有手机号、用户名唯一约束和注册幂等机制防止重复创建。
|
||
|
||
对已经存在的会员重复执行域名绑定:
|
||
|
||
- 已绑定同一域名:直接返回成功;
|
||
- 已绑定其他域名:不能由注册接口覆盖,也不提供后台改绑入口;
|
||
- 既有会员尚未绑定:保持待绑定,注册接口不做事后补绑。
|
||
|
||
## 5. 注册域名识别
|
||
|
||
### 5.1 前端采集
|
||
|
||
注册页面提交以下辅助字段:
|
||
|
||
```ts
|
||
const registerOrigin = window.location.origin;
|
||
const registerHost = window.location.host;
|
||
```
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"username": "demo_user",
|
||
"mobile": "13800000000",
|
||
"password": "******",
|
||
"registerOrigin": "https://register-a.example.com",
|
||
"registerHost": "register-a.example.com"
|
||
}
|
||
```
|
||
|
||
前端字段只用于辅助校验和问题排查,不能作为后端绑定的唯一依据,因为普通 HTTP 客户端可以伪造请求参数。
|
||
|
||
禁止保存完整 `window.location.href`,避免把邀请码、临时令牌或其他查询参数写入数据库和日志。
|
||
|
||
### 5.2 后端可信来源
|
||
|
||
后端按照部署方式解析真实注册域名。
|
||
|
||
#### 同域部署
|
||
|
||
注册页面和 API 使用同一个域名时,优先读取:
|
||
|
||
```text
|
||
HTTP Host
|
||
```
|
||
|
||
#### 前后端跨域部署
|
||
|
||
例如:
|
||
|
||
```text
|
||
注册页面:https://register-a.example.com
|
||
注册接口:https://api.example.com/api/member/register
|
||
```
|
||
|
||
此时 API 的 `Host` 是 `api.example.com`,不能用于会员绑定。浏览器跨域请求通常携带:
|
||
|
||
```text
|
||
Origin: https://register-a.example.com
|
||
```
|
||
|
||
后端应从经过 CORS 白名单校验的 `Origin` 中提取注册域名。
|
||
|
||
#### CDN 或反向代理部署
|
||
|
||
后端可能收到:
|
||
|
||
```text
|
||
X-Forwarded-Host
|
||
X-Forwarded-Proto
|
||
Forwarded
|
||
```
|
||
|
||
只有请求来自配置的可信代理 IP 时,才允许使用这些请求头。外部客户端直接传入的 `X-Forwarded-Host` 不可信。
|
||
|
||
### 5.3 建议解析优先级
|
||
|
||
```text
|
||
1. 已通过 CORS 白名单校验的 Origin 域名
|
||
2. 来自可信代理的 X-Forwarded-Host / Forwarded
|
||
3. HTTP Host
|
||
4. 前端 registerHost,仅用于一致性校验,不单独作为可信值
|
||
```
|
||
|
||
当可信来源域名与前端 `registerHost` 不一致时:
|
||
|
||
- 会员仍可注册;
|
||
- 不自动绑定域名;
|
||
- 写入安全警告和业务审计;
|
||
- 后台显示“注册域名校验异常”。
|
||
|
||
### 5.4 域名标准化
|
||
|
||
后端统一执行:
|
||
|
||
- 转换为小写;
|
||
- 移除首尾空格;
|
||
- 移除末尾的点;
|
||
- 国际化域名转换为 Punycode;
|
||
- 移除默认端口 `80` 和 `443`;
|
||
- 拒绝包含路径、用户名、密码、Fragment 的值;
|
||
- 开发环境允许配置带非默认端口的 Host;
|
||
- 生产环境建议只允许 HTTPS 注册入口。
|
||
|
||
## 6. 数据模型
|
||
|
||
域名绑定方案使用两张业务表,不再需要轮询游标表。会员登录审计另增一张独立日志表。
|
||
|
||
```text
|
||
hg_url_pool
|
||
│ 1
|
||
│
|
||
│ N
|
||
hg_member_url_binding ─── N:1 ─── hg_app_member
|
||
|
||
hg_app_member ─── 1:N ─── hg_app_member_login_log
|
||
```
|
||
|
||
### 6.1 URL 地址池表 `hg_url_pool`
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS `hg_url_pool` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||
`tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '地址名称',
|
||
`scheme` varchar(10) NOT NULL DEFAULT 'https' COMMENT '协议:http或https',
|
||
`domain` varchar(255) NOT NULL COMMENT '标准化域名,可含非默认端口',
|
||
`domain_hash` char(64) NOT NULL COMMENT '标准化域名SHA256',
|
||
`base_url` varchar(512) NOT NULL COMMENT '注册入口基础地址',
|
||
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1启用,2失效',
|
||
`bind_count` bigint unsigned NOT NULL DEFAULT 0 COMMENT '累计成功绑定次数',
|
||
`last_bound_at` datetime(3) DEFAULT NULL COMMENT '最后绑定时间',
|
||
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||
`created_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建者ID',
|
||
`updated_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '更新者ID',
|
||
`deleted_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '删除者ID',
|
||
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||
ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_url_pool_domain` (`tenant_id`,`domain_hash`),
|
||
KEY `idx_url_pool_list` (`tenant_id`,`bind_count`,`id`,`deleted_at`),
|
||
KEY `idx_url_pool_status` (`tenant_id`,`status`,`bind_count`,`deleted_at`),
|
||
KEY `idx_url_pool_deleted` (`deleted_at`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||
COMMENT='URL地址池';
|
||
```
|
||
|
||
字段说明:
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `scheme` | 注册入口协议,生产环境一般为 HTTPS |
|
||
| `domain` | 绑定和匹配使用的标准化域名 |
|
||
| `domain_hash` | 用于唯一索引,避免域名大小写或格式差异导致重复 |
|
||
| `base_url` | 后台展示、复制链接时使用,例如 `https://a.example.com/register` |
|
||
| `bind_count` | C 端注册成功并实际建立域名绑定的累计次数 |
|
||
| `last_bound_at` | 最近一次成功建立绑定的时间 |
|
||
|
||
地址一旦产生会员绑定,禁止直接修改 `domain`。需要使用新域名时应新增地址;已有会员继续保留原绑定关系。
|
||
|
||
`bind_count` 是累计使用次数。当前绑定会员数通过 `hg_member_url_binding` 按 `url_pool_id` 实时统计。正常情况下两者一致;保留累计字段便于快速排序和审计校验。
|
||
|
||
### 6.2 会员域名绑定表 `hg_member_url_binding`
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS `hg_member_url_binding` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||
`tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||
`member_id` bigint unsigned NOT NULL COMMENT '会员ID',
|
||
`url_pool_id` bigint unsigned NOT NULL COMMENT 'URL地址池ID',
|
||
`domain_snapshot` varchar(255) NOT NULL COMMENT '绑定时域名快照',
|
||
`origin_snapshot` varchar(512) NOT NULL DEFAULT '' COMMENT '绑定时Origin快照',
|
||
`bound_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '绑定时间',
|
||
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||
ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_member_url_binding` (`tenant_id`,`member_id`),
|
||
KEY `idx_member_url_pool` (`tenant_id`,`url_pool_id`,`member_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||
COMMENT='会员注册域名绑定';
|
||
```
|
||
|
||
设计说明:
|
||
|
||
- 未绑定会员没有该表记录;
|
||
- `member_id` 唯一约束保证一个会员只有一个当前绑定;
|
||
- `domain_snapshot` 用于保留绑定时的域名;
|
||
- `origin_snapshot` 只保存 Scheme 和 Host,不保存路径及查询参数;
|
||
- 绑定记录由 C 端注册事务创建,后台仅提供只读查询。
|
||
|
||
### 6.3 会员登录日志表 `hg_app_member_login_log`
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS `hg_app_member_login_log` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||
`tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||
`req_id` varchar(64) NOT NULL DEFAULT '' COMMENT '请求ID',
|
||
`member_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '会员ID,未识别会员时为0',
|
||
`username` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名快照',
|
||
`mobile_mask` varchar(32) NOT NULL DEFAULT '' COMMENT '手机号脱敏值',
|
||
`login_type` tinyint NOT NULL DEFAULT 1 COMMENT '登录方式:1账号密码,2手机验证码',
|
||
`status` tinyint NOT NULL DEFAULT 1 COMMENT '登录状态:1成功,2失败',
|
||
`err_msg` varchar(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||
`login_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '登录时间',
|
||
`login_ip_cipher` text COMMENT '登录IP密文',
|
||
`login_ip_mask` varchar(64) NOT NULL DEFAULT '' COMMENT '登录IP脱敏值',
|
||
`login_region` varchar(128) NOT NULL DEFAULT '' COMMENT '登录地区',
|
||
`login_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '登录页面标准化域名',
|
||
`user_agent` varchar(512) NOT NULL DEFAULT '' COMMENT '浏览器UA',
|
||
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_member_login_time` (`tenant_id`,`member_id`,`login_at`),
|
||
KEY `idx_member_login_status` (`tenant_id`,`status`,`login_at`),
|
||
KEY `idx_member_login_username` (`tenant_id`,`username`,`login_at`),
|
||
KEY `idx_member_login_domain` (`tenant_id`,`login_domain`,`login_at`),
|
||
KEY `idx_member_login_req` (`req_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||
COMMENT='C端会员登录日志';
|
||
```
|
||
|
||
设计说明:
|
||
|
||
- 登录成功和失败都必须写入日志;
|
||
- 登录失败且无法识别会员时,`member_id` 为 `0`,仅保存用户名快照或手机号脱敏值;
|
||
- 不保存密码、短信验证码、Token、完整手机号及登录响应正文;
|
||
- IP 仅保存密文和脱敏值,后台页面不得返回 IP 密文;
|
||
- 日志为审计数据,后台只读,不提供人工新增、编辑和删除功能;
|
||
- 操作系统和浏览器名称由 `user_agent` 在查询时解析,无需重复存储。
|
||
|
||
## 7. 字典设计
|
||
|
||
继续使用 HotGo 后端统一字典,不在前端硬编码。
|
||
|
||
```go
|
||
const (
|
||
UrlPoolStatusEnabled = 1 // 启用
|
||
UrlPoolStatusInvalid = 2 // 失效
|
||
|
||
AppMemberLoginTypePassword = 1 // 账号密码
|
||
AppMemberLoginTypeSms = 2 // 手机验证码
|
||
)
|
||
```
|
||
|
||
建议字典:
|
||
|
||
| 字典键 | 说明 |
|
||
|---|---|
|
||
| `urlPoolStatus` | URL 地址状态 |
|
||
| `appMemberLoginTypeOptions` | C 端会员登录方式,新建后端枚举字典 |
|
||
| `sys_login_status` | 登录成功/失败状态,直接复用 HotGo 已有字典 |
|
||
|
||
会员绑定状态不需要单独存储:
|
||
|
||
- 存在绑定记录:已绑定;
|
||
- 不存在绑定记录:待绑定;
|
||
- 存在绑定但地址池状态为失效:绑定域名已失效。
|
||
|
||
## 8. 注册事务设计
|
||
|
||
### 8.1 注册流程
|
||
|
||
```text
|
||
解析可信注册域名
|
||
→ 查询 URL 地址池
|
||
→ 开启事务
|
||
→ 创建会员主档
|
||
→ 初始化会员钱包
|
||
→ 如果域名存在且启用,创建绑定
|
||
→ 地址池 bind_count 加 1,并更新 last_bound_at
|
||
→ 写业务审计
|
||
→ 提交事务
|
||
```
|
||
|
||
### 8.2 错误边界
|
||
|
||
以下情况不阻断注册:
|
||
|
||
- 没有识别到域名;
|
||
- 域名未配置;
|
||
- 域名已经失效;
|
||
- 前端域名与可信来源不一致。
|
||
|
||
以上情况注册成功但会员保持待绑定,并写入审计。
|
||
|
||
以下情况必须回滚注册:
|
||
|
||
- 查询地址池时数据库异常;
|
||
- 创建会员失败;
|
||
- 初始化钱包失败;
|
||
- 已确认需要绑定,但插入绑定记录失败;
|
||
- 更新注册统计失败;
|
||
|
||
不能将数据库错误误判成“域名不存在”。
|
||
|
||
### 8.3 并发与幂等
|
||
|
||
- `hg_member_url_binding(tenant_id, member_id)` 唯一,防止重复绑定;
|
||
- `hg_url_pool(tenant_id, domain_hash)` 唯一,防止重复域名;
|
||
- 创建绑定前先查询会员现有绑定;
|
||
- 同域名重复绑定直接返回成功;
|
||
- 注册接口不允许覆盖不同域名的已有绑定。
|
||
|
||
只有 C 端注册事务实际新建绑定时才增加 `bind_count`。同一会员对同一地址的幂等重试不增加次数。
|
||
|
||
## 9. 后端服务设计
|
||
|
||
建议新增共享服务:
|
||
|
||
```go
|
||
type RegisterDomainModel struct {
|
||
Scheme string
|
||
Domain string
|
||
Origin string
|
||
Trusted bool
|
||
Source string
|
||
}
|
||
|
||
type MemberDomainResult struct {
|
||
Bound bool `json:"bound"`
|
||
UrlPoolId uint64 `json:"urlPoolId"`
|
||
Domain string `json:"domain"`
|
||
BaseUrl string `json:"baseUrl"`
|
||
StatusLabel string `json:"statusLabel"`
|
||
}
|
||
```
|
||
|
||
核心方法:
|
||
|
||
```text
|
||
ResolveRegisterDomain(ctx, registerOrigin, registerHost)
|
||
解析并校验真实注册域名
|
||
|
||
BindOnRegister(ctx, memberId, domain)
|
||
注册事务内自动绑定
|
||
|
||
GetMemberDomain(ctx, memberId)
|
||
查询会员当前绑定及域名有效状态
|
||
```
|
||
|
||
会员登录日志服务增加:
|
||
|
||
```text
|
||
PushMemberLoginLog(ctx, member, loginType, err)
|
||
收集请求上下文并推送会员登录日志
|
||
|
||
WriteMemberLoginLog(ctx, log)
|
||
队列消费或同步兜底写入日志
|
||
|
||
UpdateMemberLastLogin(ctx, memberId, ipData)
|
||
登录成功后更新会员主表最后登录信息
|
||
```
|
||
|
||
项目现有 `/site/register` 是后台管理员账号注册,不是 `hg_app_member` 的 C 端会员注册。新域名绑定应接入真正的 C 端会员注册接口。
|
||
|
||
后台当前不提供创建 C 端会员的产品功能,本方案也不新增该入口。会员及其域名绑定只能由 C 端注册流程创建。
|
||
|
||
## 10. 后台地址池管理
|
||
|
||
### 10.1 菜单位置
|
||
|
||
```text
|
||
会员管理
|
||
└── URL地址池
|
||
```
|
||
|
||
### 10.2 汇总数据
|
||
|
||
- URL 总数;
|
||
- 启用 URL 数;
|
||
- 失效 URL 数;
|
||
- 已绑定会员数;
|
||
- 待绑定会员数;
|
||
- 绑定域名已失效会员数。
|
||
|
||
### 10.3 查询条件
|
||
|
||
- 地址名称;
|
||
- 域名关键词;
|
||
- URL 状态;
|
||
- 创建时间。
|
||
|
||
### 10.4 列表字段
|
||
|
||
- 地址名称;
|
||
- 注册域名;
|
||
- 注册入口 URL;
|
||
- 状态;
|
||
- 当前绑定会员数;
|
||
- 累计绑定次数;
|
||
- 最后绑定时间;
|
||
- 备注;
|
||
- 创建时间;
|
||
- 操作。
|
||
|
||
列表默认排序固定为:
|
||
|
||
```sql
|
||
ORDER BY bind_count ASC, id ASC
|
||
```
|
||
|
||
即累计绑定次数最少的地址排在最前面;绑定次数相同时,先创建的地址排在前面。分页查询必须由后端应用该排序,不能只在前端对当前页排序。
|
||
|
||
### 10.5 操作按钮
|
||
|
||
| 按钮 | 条件 | 功能 |
|
||
|---|---|---|
|
||
| 新增地址 | 有新增权限 | 添加注册域名和入口地址 |
|
||
| Excel批量导入 | 有导入权限 | 按“地址名称、注册入口URL、备注”批量导入,导入地址统一启用 |
|
||
| 编辑 | 始终可见 | 编辑名称、入口路径和备注 |
|
||
| 启用 | 当前失效 | 手动将地址切换为启用,恢复自动注册绑定 |
|
||
| 设为失效 | 当前启用 | 手动将地址切换为失效,停止新的自动绑定 |
|
||
| 复制注册链接 | 当前启用 | 复制完整注册入口 URL |
|
||
| 详情 | 始终可见 | 进入地址详情页,查看地址信息和全部绑定会员 |
|
||
|
||
地址池不提供删除功能,历史地址统一通过“启用/失效”管理。启用和失效由管理员手动点击切换。状态变更成功后立即刷新列表,并写入业务审计。地址变为失效后:
|
||
|
||
- 不再接受新的注册自动绑定;
|
||
- 已有会员绑定关系保留;
|
||
- 详情页仍然可以查看原有绑定会员。
|
||
|
||
### 10.6 地址详情页
|
||
|
||
点击列表中的“详情”进入独立详情页。页面分为地址信息和绑定会员两个区域。
|
||
|
||
地址信息展示:
|
||
|
||
- 地址名称;
|
||
- 注册域名;
|
||
- 注册入口 URL;
|
||
- 当前状态;
|
||
- 累计绑定次数;
|
||
- 当前绑定会员数;
|
||
- 最后绑定时间;
|
||
- 创建人、创建时间;
|
||
- 备注。
|
||
|
||
绑定会员列表展示当前绑定该地址的全部会员,并支持分页。字段包括:
|
||
|
||
- 会员编号;
|
||
- 用户名;
|
||
- 手机号脱敏值;
|
||
- 会员状态;
|
||
- 绑定时间。
|
||
|
||
详情页会员列表支持按用户名、手机号和绑定时间查询。这里的“全部用户”指绑定该地址的全部会员,列表仅供查看,不提供添加、解绑或改绑操作。
|
||
|
||
## 11. 会员列表扩展
|
||
|
||
会员列表增加字段:
|
||
|
||
- 注册域名;
|
||
- 域名绑定状态;
|
||
- 绑定时间。
|
||
|
||
增加筛选:
|
||
|
||
| 筛选值 | 条件 |
|
||
|---|---|
|
||
| 全部 | 不限制 |
|
||
| 已绑定 | 存在绑定且域名启用 |
|
||
| 待绑定 | 不存在绑定记录 |
|
||
| 域名已失效 | 存在绑定但域名已失效 |
|
||
|
||
操作栏不增加绑定、解绑或重新绑定按钮。会员详情可只读查看域名信息。
|
||
|
||
会员详情增加“注册域名信息”区域,展示:
|
||
|
||
- 当前域名;
|
||
- 注册入口;
|
||
- 域名状态;
|
||
- 绑定时间。
|
||
|
||
## 12. 会员登录与日志设计
|
||
|
||
### 12.1 建设范围
|
||
|
||
当前项目的登录日志分为两类理解:
|
||
|
||
- `hg_sys_login_log` 及“系统登录日志”页面记录的是后台管理员登录。现有后台账号、手机号登录均会调用 `SysLoginLog().Push()` 写入成功或失败结果;
|
||
- `hg_app_member` 虽然预留了 `last_login_at`、`last_login_ip_cipher`、`last_login_ip_mask`、`last_login_region` 字段,会员列表也展示最后登录信息,但当前代码中没有找到 C 端会员登录接口对这些字段的更新逻辑;
|
||
- 当前也没有独立的会员登录历史表或会员登录日志列表。
|
||
|
||
因此,现状结论是:**后台管理员登录有日志,C 端会员登录目前没有完整可用的日志功能**。
|
||
|
||
本次将以下两项能力纳入正式实施范围:
|
||
|
||
1. 登录成功后更新会员主表中的最后登录时间、IP 脱敏值和地区;
|
||
2. 使用独立的 `hg_app_member_login_log` 记录每次会员登录成功或失败,避免与后台管理员的 `hg_sys_login_log` 混用。
|
||
|
||
### 12.2 登录成功后的会员主表更新
|
||
|
||
会员身份校验通过后、返回登录成功响应前,更新 `hg_app_member`:
|
||
|
||
| 字段 | 更新内容 |
|
||
|---|---|
|
||
| `last_login_at` | 当前登录成功时间 |
|
||
| `last_login_ip_cipher` | 使用项目统一加密工具保存的客户端 IP 密文 |
|
||
| `last_login_ip_mask` | 用于后台展示的 IP 脱敏值 |
|
||
| `last_login_region` | 根据客户端 IP 解析的国家/省/市摘要 |
|
||
|
||
处理规则:
|
||
|
||
- 只有登录成功才更新上述字段,登录失败不得覆盖会员最后一次成功登录信息;
|
||
- 客户端 IP 使用项目现有 `location.GetClientIp()` 识别,并遵守可信代理配置;
|
||
- 地区解析失败不阻断登录,地区保存为空,同时记录 Warning 日志;
|
||
- 会员主表更新失败时不签发 Token,向客户端返回“登录失败,请稍后重试”;
|
||
- 更新字段必须使用 DAO 列名,禁止硬编码字段字符串;
|
||
- 不在业务日志中输出 IP 密文、密码、短信验证码或 Token。
|
||
|
||
### 12.3 会员登录日志写入流程
|
||
|
||
所有 C 端会员登录入口必须调用同一个会员登录日志服务,不允许每种登录方式自行拼装日志。
|
||
|
||
```text
|
||
收到登录请求
|
||
→ 解析请求ID、登录方式、客户端IP、地区、域名和User-Agent
|
||
→ 校验会员及登录凭证
|
||
→ 失败:写入失败日志,返回统一登录错误
|
||
→ 成功:更新会员最后登录信息
|
||
→ 签发Token
|
||
→ 写入成功日志
|
||
→ 返回登录结果
|
||
```
|
||
|
||
为保证失败请求也能记录,登录控制器或登录服务应使用统一的收口逻辑,在方法返回前根据最终 `err` 和会员识别结果生成日志。日志优先通过项目消息队列异步写入;队列推送失败时同步写库兜底,并记录 Warning。日志写入最终失败不得把已经完成的身份认证改成登录失败,但必须留下服务端告警。
|
||
|
||
失败原因只记录可审计的标准化文案,例如:
|
||
|
||
- 账号或密码错误;
|
||
- 验证码错误或已过期;
|
||
- 会员不存在;
|
||
- 会员已禁用;
|
||
- 风控限制登录;
|
||
- 系统异常。
|
||
|
||
### 12.4 后台会员登录日志页面
|
||
|
||
菜单位置:
|
||
|
||
```text
|
||
会员管理
|
||
└── 会员登录日志
|
||
```
|
||
|
||
查询条件:
|
||
|
||
- 会员编号、用户名或手机号关键词;
|
||
- 登录方式;
|
||
- 登录状态;
|
||
- 登录域名;
|
||
- IP 脱敏值;
|
||
- 登录时间范围。
|
||
|
||
列表默认按照 `login_at DESC, id DESC` 排序,展示:
|
||
|
||
- 日志ID;
|
||
- 会员编号;
|
||
- 用户名;
|
||
- 手机号脱敏值;
|
||
- 登录方式;
|
||
- 登录状态;
|
||
- 登录时间;
|
||
- 登录IP脱敏值;
|
||
- 登录地区;
|
||
- 登录域名;
|
||
- 操作系统;
|
||
- 浏览器;
|
||
- 失败原因;
|
||
- 操作。
|
||
|
||
操作栏只提供“详情”。列表顶部提供“导出”,导出内容与当前查询条件一致。详情弹窗补充展示请求ID和完整 User-Agent,但不展示 IP 密文及任何登录凭证。
|
||
|
||
列表查询必须按 `tenant_id` 隔离数据。使用完整手机号查询时,先在会员表中匹配会员ID,再按 `member_id` 查询日志;对无法识别会员的失败记录,只支持按已保存的手机号脱敏值查询。
|
||
|
||
## 13. 接口设计
|
||
|
||
### 13.1 后台接口
|
||
|
||
| 接口 | 方法 | 说明 |
|
||
|---|---|---|
|
||
| `/urlPool/list` | GET | 地址池分页列表 |
|
||
| `/urlPool/view` | GET | 地址详情及绑定统计 |
|
||
| `/urlPool/edit` | POST | 新增或编辑地址 |
|
||
| `/urlPool/import` | POST | Excel批量导入地址,成功记录统一设为启用 |
|
||
| `/urlPool/status` | POST | 启用或设为失效 |
|
||
| `/urlPool/memberList` | GET | 分页查看域名当前绑定的全部会员 |
|
||
| `/urlPool/summary` | GET | 地址池汇总数据 |
|
||
| `/appMemberLoginLog/list` | GET | 会员登录日志分页列表 |
|
||
| `/appMemberLoginLog/view` | GET | 会员登录日志详情 |
|
||
| `/appMemberLoginLog/export` | GET | 按当前条件导出会员登录日志 |
|
||
|
||
### 13.2 C 端接口
|
||
|
||
注册请求增加:
|
||
|
||
```json
|
||
{
|
||
"registerOrigin": "https://register-a.example.com",
|
||
"registerHost": "register-a.example.com"
|
||
}
|
||
```
|
||
|
||
会员查询当前域名响应建议:
|
||
|
||
```json
|
||
{
|
||
"bound": true,
|
||
"domain": "register-a.example.com",
|
||
"baseUrl": "https://register-a.example.com/register",
|
||
"domainStatus": 1,
|
||
"domainStatusLabel": "启用"
|
||
}
|
||
```
|
||
|
||
未绑定时:
|
||
|
||
```json
|
||
{
|
||
"bound": false,
|
||
"domain": "",
|
||
"baseUrl": "",
|
||
"domainStatus": 0,
|
||
"domainStatusLabel": "待绑定"
|
||
}
|
||
```
|
||
|
||
所有 C 端会员登录接口的响应格式保持不变,日志采集由服务端统一完成,不要求前端额外提交 IP、地区或 User-Agent。跨域部署时,前端可提交 `loginHost` 作为辅助校验值,但最终登录域名仍按可信 `Origin`、可信代理转发头和 `Host` 解析。
|
||
|
||
## 14. HotGo 模块结构
|
||
|
||
建议作为主模块开发:
|
||
|
||
```text
|
||
server/
|
||
├── api/admin/urlpool/urlpool.go
|
||
├── api/admin/appmemberloginlog/appmemberloginlog.go
|
||
├── internal/consts/url_pool.go
|
||
├── internal/consts/app_member_login_log.go
|
||
├── internal/controller/admin/sys/url_pool.go
|
||
├── internal/controller/admin/sys/app_member_login_log.go
|
||
├── internal/logic/sys/url_pool.go
|
||
├── internal/logic/sys/app_member_login_log.go
|
||
├── internal/model/input/sysin/url_pool.go
|
||
├── internal/model/input/sysin/app_member_login_log.go
|
||
├── internal/router/genrouter/url_pool.go
|
||
├── internal/router/genrouter/app_member_login_log.go
|
||
├── internal/queues/app_member_login_log.go
|
||
├── internal/service/sys.go # 追加 ISysUrlPool、ISysAppMemberLoginLog
|
||
├── internal/dao/url_pool.go # CLI 生成
|
||
├── internal/dao/member_url_binding.go # CLI 生成
|
||
├── internal/dao/app_member_login_log.go # CLI 生成
|
||
├── storage/data/migrations/<date>_url_pool.sql
|
||
└── storage/data/migrations/<date>_app_member_login_log.sql
|
||
|
||
web/src/
|
||
├── api/urlPool/index.ts
|
||
├── api/appMemberLoginLog/index.ts
|
||
├── views/app-member/url-pool/
|
||
├── index.vue
|
||
├── edit.vue
|
||
├── detail.vue
|
||
└── model.ts
|
||
└── views/app-member/login-log/
|
||
├── index.vue
|
||
├── detail.vue
|
||
└── model.ts
|
||
```
|
||
|
||
DAO、Entity、DO 和 DAO Internal 必须通过 GoFrame CLI 生成,不手工修改。
|
||
|
||
## 15. 权限设计
|
||
|
||
```text
|
||
/urlPool/list
|
||
/urlPool/view
|
||
/urlPool/edit
|
||
/urlPool/import
|
||
/urlPool/status
|
||
/urlPool/memberList
|
||
/urlPool/summary
|
||
/appMemberLoginLog/list
|
||
/appMemberLoginLog/view
|
||
/appMemberLoginLog/export
|
||
```
|
||
|
||
域名启用/失效和注册自动绑定均写入业务审计。
|
||
|
||
## 16. 缓存设计
|
||
|
||
会员当前域名可以使用项目统一缓存:
|
||
|
||
```text
|
||
urlpool:member:{tenantId}:{memberId}
|
||
```
|
||
|
||
地址池域名查询可以缓存:
|
||
|
||
```text
|
||
urlpool:domain:{tenantId}:{domainHash}
|
||
```
|
||
|
||
建议缓存 10~30 分钟。以下操作后主动清除缓存:
|
||
|
||
- 地址编辑;
|
||
- 地址启用或失效;
|
||
- 注册自动绑定;
|
||
|
||
缓存统一使用 `cache.Instance()`,数据库仍是唯一事实来源。
|
||
|
||
## 17. 安全要求
|
||
|
||
1. 不得只信任前端 `registerHost` 参数。
|
||
2. `Origin` 必须通过地址池或 CORS 白名单校验。
|
||
3. 转发请求头只能在可信代理来源下使用。
|
||
4. 后台保存域名前必须进行标准化和 URL 解析。
|
||
5. 禁止保存包含用户名、密码、Token 的 URL。
|
||
6. 日志只记录标准化域名和 Origin,不记录完整注册查询参数。
|
||
7. 后台不得提供会员域名绑定、解绑或改绑接口。
|
||
8. 生产环境建议只允许 HTTPS 注册入口。
|
||
9. 域名变更不得静默覆盖已有会员绑定。
|
||
10. 未知或校验异常域名不自动创建地址池记录。
|
||
11. 会员登录日志不得保存密码、短信验证码、Token、完整手机号或登录响应正文。
|
||
12. 后台日志接口不得返回 `last_login_ip_cipher` 或 `login_ip_cipher`。
|
||
13. 失败日志使用统一错误文案,避免暴露账号是否存在等敏感认证细节。
|
||
14. 会员登录日志列表、详情和导出必须执行租户隔离,禁止跨租户查询。
|
||
|
||
## 18. 异常处理
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| 地址池为空 | 注册成功,会员待绑定 |
|
||
| 注册域名未配置 | 注册成功,会员待绑定并写审计 |
|
||
| 注册域名失效 | 注册成功,会员待绑定并写审计 |
|
||
| 前端域名与可信来源不一致 | 注册成功但不绑定,记录安全警告 |
|
||
| 域名重复添加 | 拒绝保存 |
|
||
| 已绑定同一域名 | 幂等返回成功 |
|
||
| 注册接口试图覆盖其他域名 | 拒绝覆盖,保留原绑定关系 |
|
||
| 地址池查询数据库异常 | 注册失败并回滚 |
|
||
| 创建绑定失败 | 注册事务回滚 |
|
||
| 已产生绑定的域名被直接修改 | 拒绝修改域名字段 |
|
||
| 列表绑定次数相同 | 按地址ID升序稳定排序 |
|
||
| 登录成功但会员最后登录信息更新失败 | 不签发Token,返回通用系统错误并记录失败日志 |
|
||
| 登录IP地区解析失败 | 登录继续,地区留空并记录Warning |
|
||
| 登录日志队列推送失败 | 同步写入日志表兜底并记录Warning |
|
||
| 队列与同步写入均失败 | 不撤销已完成登录,记录服务端告警供运维处理 |
|
||
| 登录失败且无法识别会员 | 以 `member_id=0` 写入脱敏失败日志 |
|
||
|
||
## 19. 验收标准
|
||
|
||
### 19.1 注册绑定
|
||
|
||
- 用户通过 A 域名注册后,会员绑定 A。
|
||
- 用户通过 B 域名注册后,会员绑定 B。
|
||
- URL 路径和查询参数不同但域名相同,绑定结果相同。
|
||
- 默认端口不会造成重复域名。
|
||
- 地址池为空时会员仍注册成功并显示待绑定。
|
||
- 未配置域名和失效域名不会建立自动绑定。
|
||
|
||
### 19.2 部署场景
|
||
|
||
- 前后端同域部署能够从 Host 正确识别域名。
|
||
- 前后端跨域部署能够从通过校验的 Origin 正确识别域名。
|
||
- CDN/反向代理部署能够从可信转发头正确识别域名。
|
||
- 外部客户端伪造 `X-Forwarded-Host` 不会产生错误绑定。
|
||
- 前端伪造 `registerHost` 不会覆盖后端可信域名。
|
||
|
||
### 19.3 并发和幂等
|
||
|
||
- 并发提交不会为同一会员创建多条绑定。
|
||
- 相同注册请求重试不会重复创建会员或绑定。
|
||
- 已绑定会员不能通过普通注册接口更换域名。
|
||
|
||
### 19.4 后台管理
|
||
|
||
- 可以新增、编辑、启用和失效注册域名。
|
||
- 可以使用 Excel 批量导入地址,表头包含“地址名称、注册入口URL、备注”,所有导入地址默认为启用。
|
||
- 可以手动点击“启用”或“设为失效”,状态立即生效。
|
||
- 地址池列表默认按累计绑定次数升序排列。
|
||
- 可以复制完整注册链接。
|
||
- 点击地址详情可以分页查看当前绑定的全部会员。
|
||
- 每次实际建立新绑定时,地址累计绑定次数准确加一。
|
||
- 幂等重试不会重复增加累计绑定次数。
|
||
- 后台不存在创建会员入口。
|
||
- 后台不存在会员域名绑定、解绑或重新绑定入口。
|
||
- 失效域名不允许新增绑定。
|
||
- 所有关键操作均可在业务审计中查询。
|
||
|
||
### 19.5 会员登录日志
|
||
|
||
- 会员登录成功后,会员主表最后登录时间、IP密文、IP脱敏值和地区正确更新。
|
||
- 登录失败不会覆盖会员最后一次成功登录信息。
|
||
- 账号密码登录和手机验证码登录均写入独立会员登录日志。
|
||
- 登录成功、密码错误、验证码错误、账号禁用和系统异常场景均能生成对应日志。
|
||
- 无法识别会员的失败请求以 `member_id=0` 记录,且不保存完整手机号。
|
||
- 后台“会员登录日志”可以按会员、方式、状态、域名、IP和时间查询。
|
||
- 日志列表默认按登录时间倒序排列,可以查看详情并按筛选条件导出。
|
||
- 后台页面和导出文件均不出现IP密文、密码、验证码或Token。
|
||
- 登录日志队列异常时会同步写库兜底;两种写入都失败时产生服务端 Warning 告警。
|
||
|
||
## 20. 待确认事项
|
||
|
||
1. 生产环境是否强制只允许 HTTPS。
|
||
2. 前端页面和注册 API 是同域部署还是跨域部署。
|
||
3. 是否存在 App、桌面客户端等没有浏览器 `Origin` 的注册入口。
|
||
4. 注册入口是否统一为 `/register`,还是每个域名可以配置不同路径。
|
||
5. 地址池是否需要按业务线、渠道或租户进一步分组。
|
||
6. 会员登录日志的在线保留时长,以及是否需要定时归档历史日志。
|