33 KiB
URL 地址池与会员注册域名绑定设计方案
文档状态:方案设计 方案版本:v2.1 最后更新:2026-10-15 适用项目:Olons Admin / HotGo 关联模块:会员注册、会员登录、会员管理、URL 地址池管理、会员登录日志
1. 方案变更说明
原方案采用“会员注册时从 URL 地址池轮询取出一个 URL”的模式。现调整为“会员通过哪个域名打开注册页面,就与哪个域名建立绑定关系”。
新流程如下:
管理员或推广人员先向用户提供一个注册链接
→ 用户通过该链接打开注册页面
→ 注册页面获取浏览器地址栏中的域名
→ 用户提交注册
→ 后端校验本次请求的真实注册域名
→ 创建会员
→ 建立会员与注册域名的绑定关系
本方案不再使用 URL 轮询顺序、分配游标和自动轮询算法。URL 地址池的作用调整为:
- 维护允许用于会员注册的域名;
- 管理域名的启用、失效状态;
- 统计每个域名带来的注册会员;
- 查询会员与注册域名的绑定关系。
每个地址同时维护累计绑定次数。地址池列表默认把绑定次数较少的地址排在前面;点击地址详情可以查看当前绑定该地址的全部会员。
2. 设计目标
- 用户使用某个域名注册时,自动与该域名绑定。
- 注册域名由后端根据可信请求信息确认,不能只信任前端传入值。
- 每个会员只能存在一个当前绑定域名。
- 域名未配置或已失效时,会员仍允许注册成功,但保持“待绑定”。
- 后台不提供创建会员、手动绑定或重新绑定域名的入口。
- 域名失效后不再接受新的自动绑定,但保留原有绑定用于审计。
- 支持同域部署、前后端跨域部署以及 CDN/反向代理部署。
- 所有注册自动绑定和地址状态变更都有业务审计记录。
- 每个地址记录累计绑定次数,地址池列表默认按绑定次数升序排列。
- 后台可以手动将启用地址切换为失效,也可以将失效地址重新启用。
- C 端会员登录成功后更新会员最后登录时间、IP脱敏信息和地区。
- C 端会员每次登录成功或失败均记录独立日志,并可在后台查询、查看和导出。
3. 核心概念
3.1 注册 URL
管理员或推广人员提供给用户的完整注册链接,例如:
https://register-a.example.com/register?inviteCode=ABC123
完整 URL 中可能包含路径、邀请码或其他查询参数,但会员绑定只取域名部分:
register-a.example.com
默认端口不作为域名的一部分:
https://register-a.example.com:443 → register-a.example.com
http://register-a.example.com:80 → register-a.example.com
非默认端口的测试环境可以保留端口:
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 正常注册绑定
用户打开 https://register-a.example.com/register
→ 前端读取 window.location.origin
→ 用户提交注册
→ 后端确认真实来源域名为 register-a.example.com
→ 查询地址池
→ 地址存在且启用
→ 创建会员及钱包
→ 创建会员与 register-a.example.com 的绑定
→ 注册成功
4.2 地址池没有该域名
用户通过 unknown.example.com 注册
→ 后端未在地址池找到该域名
→ 会员和钱包正常创建
→ 不创建域名绑定记录
→ 记录“未知注册域名”业务审计
→ 会员显示为待绑定
4.3 域名已经失效
用户通过已失效域名注册
→ 会员正常创建
→ 不建立自动绑定
→ 记录“通过失效域名注册”业务审计
→ 会员显示为待绑定
4.4 重复注册请求
同一会员创建请求因网络原因重复提交时,应通过现有手机号、用户名唯一约束和注册幂等机制防止重复创建。
对已经存在的会员重复执行域名绑定:
- 已绑定同一域名:直接返回成功;
- 已绑定其他域名:不能由注册接口覆盖,也不提供后台改绑入口;
- 既有会员尚未绑定:保持待绑定,注册接口不做事后补绑。
5. 注册域名识别
5.1 前端采集
注册页面提交以下辅助字段:
const registerOrigin = window.location.origin;
const registerHost = window.location.host;
请求示例:
{
"username": "demo_user",
"mobile": "13800000000",
"password": "******",
"registerOrigin": "https://register-a.example.com",
"registerHost": "register-a.example.com"
}
前端字段只用于辅助校验和问题排查,不能作为后端绑定的唯一依据,因为普通 HTTP 客户端可以伪造请求参数。
禁止保存完整 window.location.href,避免把邀请码、临时令牌或其他查询参数写入数据库和日志。
5.2 后端可信来源
后端按照部署方式解析真实注册域名。
同域部署
注册页面和 API 使用同一个域名时,优先读取:
HTTP Host
前后端跨域部署
例如:
注册页面:https://register-a.example.com
注册接口:https://api.example.com/api/member/register
此时 API 的 Host 是 api.example.com,不能用于会员绑定。浏览器跨域请求通常携带:
Origin: https://register-a.example.com
后端应从经过 CORS 白名单校验的 Origin 中提取注册域名。
CDN 或反向代理部署
后端可能收到:
X-Forwarded-Host
X-Forwarded-Proto
Forwarded
只有请求来自配置的可信代理 IP 时,才允许使用这些请求头。外部客户端直接传入的 X-Forwarded-Host 不可信。
5.3 建议解析优先级
1. 已通过 CORS 白名单校验的 Origin 域名
2. 来自可信代理的 X-Forwarded-Host / Forwarded
3. HTTP Host
4. 前端 registerHost,仅用于一致性校验,不单独作为可信值
当可信来源域名与前端 registerHost 不一致时:
- 会员仍可注册;
- 不自动绑定域名;
- 写入安全警告和业务审计;
- 后台显示“注册域名校验异常”。
5.4 域名标准化
后端统一执行:
- 转换为小写;
- 移除首尾空格;
- 移除末尾的点;
- 国际化域名转换为 Punycode;
- 移除默认端口
80和443; - 拒绝包含路径、用户名、密码、Fragment 的值;
- 开发环境允许配置带非默认端口的 Host;
- 生产环境建议只允许 HTTPS 注册入口。
6. 数据模型
域名绑定方案使用两张业务表,不再需要轮询游标表。会员登录审计另增一张独立日志表。
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
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
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
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 后端统一字典,不在前端硬编码。
const (
UrlPoolStatusEnabled = 1 // 启用
UrlPoolStatusInvalid = 2 // 失效
AppMemberLoginTypePassword = 1 // 账号密码
AppMemberLoginTypeSms = 2 // 手机验证码
)
建议字典:
| 字典键 | 说明 |
|---|---|
urlPoolStatus |
URL 地址状态 |
appMemberLoginTypeOptions |
C 端会员登录方式,新建后端枚举字典 |
sys_login_status |
登录成功/失败状态,直接复用 HotGo 已有字典 |
会员绑定状态不需要单独存储:
- 存在绑定记录:已绑定;
- 不存在绑定记录:待绑定;
- 存在绑定但地址池状态为失效:绑定域名已失效。
8. 注册事务设计
8.1 注册流程
解析可信注册域名
→ 查询 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. 后端服务设计
建议新增共享服务:
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"`
}
核心方法:
ResolveRegisterDomain(ctx, registerOrigin, registerHost)
解析并校验真实注册域名
BindOnRegister(ctx, memberId, domain)
注册事务内自动绑定
GetMemberDomain(ctx, memberId)
查询会员当前绑定及域名有效状态
会员登录日志服务增加:
PushMemberLoginLog(ctx, member, loginType, err)
收集请求上下文并推送会员登录日志
WriteMemberLoginLog(ctx, log)
队列消费或同步兜底写入日志
UpdateMemberLastLogin(ctx, memberId, ipData)
登录成功后更新会员主表最后登录信息
项目现有 /site/register 是后台管理员账号注册,不是 hg_app_member 的 C 端会员注册。新域名绑定应接入真正的 C 端会员注册接口。
后台当前不提供创建 C 端会员的产品功能,本方案也不新增该入口。会员及其域名绑定只能由 C 端注册流程创建。
10. 后台地址池管理
10.1 菜单位置
会员管理
└── URL地址池
10.2 汇总数据
- URL 总数;
- 启用 URL 数;
- 失效 URL 数;
- 已绑定会员数;
- 待绑定会员数;
- 绑定域名已失效会员数。
10.3 查询条件
- 地址名称;
- 域名关键词;
- URL 状态;
- 创建时间。
10.4 列表字段
- 地址名称;
- 注册域名;
- 注册入口 URL;
- 状态;
- 当前绑定会员数;
- 累计绑定次数;
- 最后绑定时间;
- 备注;
- 创建时间;
- 操作。
列表默认排序固定为:
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 端会员登录目前没有完整可用的日志功能。
本次将以下两项能力纳入正式实施范围:
- 登录成功后更新会员主表中的最后登录时间、IP 脱敏值和地区;
- 使用独立的
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 端会员登录入口必须调用同一个会员登录日志服务,不允许每种登录方式自行拼装日志。
收到登录请求
→ 解析请求ID、登录方式、客户端IP、地区、域名和User-Agent
→ 校验会员及登录凭证
→ 失败:写入失败日志,返回统一登录错误
→ 成功:更新会员最后登录信息
→ 签发Token
→ 写入成功日志
→ 返回登录结果
为保证失败请求也能记录,登录控制器或登录服务应使用统一的收口逻辑,在方法返回前根据最终 err 和会员识别结果生成日志。日志优先通过项目消息队列异步写入;队列推送失败时同步写库兜底,并记录 Warning。日志写入最终失败不得把已经完成的身份认证改成登录失败,但必须留下服务端告警。
失败原因只记录可审计的标准化文案,例如:
- 账号或密码错误;
- 验证码错误或已过期;
- 会员不存在;
- 会员已禁用;
- 风控限制登录;
- 系统异常。
12.4 后台会员登录日志页面
菜单位置:
会员管理
└── 会员登录日志
查询条件:
- 会员编号、用户名或手机号关键词;
- 登录方式;
- 登录状态;
- 登录域名;
- 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 端接口
注册请求增加:
{
"registerOrigin": "https://register-a.example.com",
"registerHost": "register-a.example.com"
}
会员查询当前域名响应建议:
{
"bound": true,
"domain": "register-a.example.com",
"baseUrl": "https://register-a.example.com/register",
"domainStatus": 1,
"domainStatusLabel": "启用"
}
未绑定时:
{
"bound": false,
"domain": "",
"baseUrl": "",
"domainStatus": 0,
"domainStatusLabel": "待绑定"
}
所有 C 端会员登录接口的响应格式保持不变,日志采集由服务端统一完成,不要求前端额外提交 IP、地区或 User-Agent。跨域部署时,前端可提交 loginHost 作为辅助校验值,但最终登录域名仍按可信 Origin、可信代理转发头和 Host 解析。
14. HotGo 模块结构
建议作为主模块开发:
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. 权限设计
/urlPool/list
/urlPool/view
/urlPool/edit
/urlPool/import
/urlPool/status
/urlPool/memberList
/urlPool/summary
/appMemberLoginLog/list
/appMemberLoginLog/view
/appMemberLoginLog/export
域名启用/失效和注册自动绑定均写入业务审计。
16. 缓存设计
会员当前域名可以使用项目统一缓存:
urlpool:member:{tenantId}:{memberId}
地址池域名查询可以缓存:
urlpool:domain:{tenantId}:{domainHash}
建议缓存 10~30 分钟。以下操作后主动清除缓存:
- 地址编辑;
- 地址启用或失效;
- 注册自动绑定;
缓存统一使用 cache.Instance(),数据库仍是唯一事实来源。
17. 安全要求
- 不得只信任前端
registerHost参数。 Origin必须通过地址池或 CORS 白名单校验。- 转发请求头只能在可信代理来源下使用。
- 后台保存域名前必须进行标准化和 URL 解析。
- 禁止保存包含用户名、密码、Token 的 URL。
- 日志只记录标准化域名和 Origin,不记录完整注册查询参数。
- 后台不得提供会员域名绑定、解绑或改绑接口。
- 生产环境建议只允许 HTTPS 注册入口。
- 域名变更不得静默覆盖已有会员绑定。
- 未知或校验异常域名不自动创建地址池记录。
- 会员登录日志不得保存密码、短信验证码、Token、完整手机号或登录响应正文。
- 后台日志接口不得返回
last_login_ip_cipher或login_ip_cipher。 - 失败日志使用统一错误文案,避免暴露账号是否存在等敏感认证细节。
- 会员登录日志列表、详情和导出必须执行租户隔离,禁止跨租户查询。
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. 待确认事项
- 生产环境是否强制只允许 HTTPS。
- 前端页面和注册 API 是同域部署还是跨域部署。
- 是否存在 App、桌面客户端等没有浏览器
Origin的注册入口。 - 注册入口是否统一为
/register,还是每个域名可以配置不同路径。 - 地址池是否需要按业务线、渠道或租户进一步分组。
- 会员登录日志的在线保留时长,以及是否需要定时归档历史日志。