# 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/_url_pool.sql └── storage/data/migrations/_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. 会员登录日志的在线保留时长,以及是否需要定时归档历史日志。