diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md index 4b63f700..9c70171d 100644 --- a/Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md +++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md @@ -4,7 +4,7 @@ > **版本**: v1.0 > **日期**: 2026-04-24 > **关联模块**: `apps/accounts/` — 账号认证、登录安全、密码管理 -> **关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v1.3) +> **关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v2.0) > **关联技术方案**: `Project/fonrey/TECH_STACK/登录管理技术方案.md` --- @@ -13,17 +13,18 @@ ### 核心概念 -- **UserAccount(用户账号)**:系统登录主体,必须与员工档案(`org.Staff`)1:1 绑定。分为 Tenant Admin(超级管理账号,每租户唯一)和普通员工账号(username 固定为手机号)。 +- **UserAccount(用户账号)**:系统登录主体,必须与员工档案(`org.Staff`)1:1 绑定。分为 Tenant Admin(超级管理账号,每租户唯一,username 固定为联系人手机号)和普通员工账号(username 固定为员工手机号)。 - **LoginAttempt(登录尝试记录)**:记录每次登录行为(成功/失败),用于安全审计和账号锁定判断,保留 ≥ 90 天。 -- **PasswordResetToken(密码重置令牌)**:通过邮件找回密码时生成的一次性令牌,有效期 30 分钟,使用后立即失效。 +- **PasswordResetToken(密码重置令牌)**:~~通过邮件找回密码时生成的一次性令牌~~ 已废弃,详见 `SmsOtpRecord`。 +- **SmsOtpRecord(短信验证码记录)**:找回密码和手机验证码登录时向用户手机号发送的 6 位一次性验证码。用 `scene` 字段区分场景(`password_reset` / `login`),有效期按场景不同(找回密码 10 分钟,验证码登录 5 分钟);找回密码验证通过后颁发 `sms_reset_token`(有效期 15 分钟),验证码登录验证通过后直接颁发 Session Token;使用后立即作废。 - **PasswordHistory(历史密码记录)**:保存最近 3 次密码哈希,用于防止重复使用历史密码。 ### 关键业务规则 1. **账号与员工强绑定**:每个登录账号 **必须** 与 `org.Staff` 中的员工档案 1:1 绑定(Tenant Admin 例外,可不绑定)。 -2. **用户名规则差异化**: - - Tenant Admin:由平台运营自定义(字母开头,6~30 位,含字母/数字/下划线) - - 普通员工:**固定为员工手机号**(11 位数字),创建后不可变更 +2. **用户名规则统一为手机号**: + - Tenant Admin:**固定为该租户联系人的手机号**(11 位数字),来源于 `public.tenants.contact_phone`,全局唯一,创建后不可更改 + - 普通员工:**固定为员工手机号**(11 位数字),同租户内唯一,创建后不可变更 3. **初始密码强制修改**:新账号及密码重置后,`is_initial_password = True`,首次登录必须修改密码,不可跳过。 4. **账号锁定机制**:同一账号连续密码错误 ≥ 5 次,状态置为 `locked`,30 分钟后自动恢复;Tenant Admin 可提前手动解锁。 5. **员工离职联动**:员工离职时,对应账号的 `status` 自动置为 `disabled`,不可登录,历史操作记录保留。 @@ -38,7 +39,7 @@ UserAccount │ ├── 1:1 ── org.Staff (实名绑定,普通员工必须) ├── 1:N ── LoginAttempt (登录审计记录) - ├── 1:N ── PasswordResetToken (密码重置令牌) + ├── 1:N ── SmsOtpRecord (短信验证码记录,找回密码用) ├── 1:N ── PasswordHistory (历史密码记录) └── M:1 ── UserAccount.created_by (创建人自引用) ``` @@ -49,10 +50,10 @@ UserAccount |----|------------|------| | `user_accounts` | 租户 Schema | 账号数据按租户隔离,username 唯一性在 Schema 维度生效 | | `login_attempts` | 租户 Schema | 审计记录属于租户,跨租户不可见 | -| `password_reset_tokens` | 租户 Schema | 令牌与租户账号绑定 | +| `sms_otp_records` | 租户 Schema | 短信验证码记录,找回密码用 | | `password_histories` | 租户 Schema | 历史密码与账号绑定 | -> **注意**:Tenant ID 验证相关逻辑在 **Public Schema**(`shared_apps`),使用 `django-tenants` 的 `TenantModel`,不在本文档范围内,详见 `DATA_MODEL.md` §四(公共 Schema)。 +> **注意**:Tenant Code 验证相关逻辑在 **Public Schema**(`shared_apps`),使用 `django-tenants` 的 `TenantModel`,不在本文档范围内,详见 `DATA_MODEL.md` §四(公共 Schema)。 --- @@ -69,7 +70,7 @@ UserAccount | `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键(审计场景下 BigInt 更直观;跨环境引用使用 UUID 扩展字段见下) | | `username` | `VARCHAR(30)` | `NOT NULL` | — | 登录名;普通员工为手机号(11 位数字);Tenant Admin 为自定义字符串;创建后不可更改 | | `password` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希存储,使用 Django `make_password` | -| `email` | `VARCHAR(254)` | `NULL` | `NULL` | 绑定邮箱;用于找回密码/用户名;为空则无法自助找回;同租户唯一 | +| `email` | `VARCHAR(254)` | `NULL` | `NULL` | 绑定邮箱;完全可选,在本系统无任何必须业务用途;若填写则在同租户内唯一 | | `phone_enc` | `TEXT` | `NULL` | `NULL` | 手机号 AES-256-GCM 加密密文(`core.encryption`);普通员工必填 | | `phone_hash` | `VARCHAR(64)` | `NULL` | `NULL` | 手机号 SHA-256 哈希;用于唯一性校验和查询;不可反推原文 | | `staff_id` | `BIGINT` | `FK → org_staff.id`, `NULL`, `UNIQUE` | `NULL` | 员工档案绑定(1:1);普通员工必须有值;Tenant Admin 可为空 | @@ -122,8 +123,8 @@ class UserAccountManager(BaseUserManager): class UserAccount(AbstractBaseUser): """ 租户级用户账号。 - - 普通员工:username 固定为手机号(11 位数字) - - Tenant Admin:username 由平台运营自定义(字母开头,6~30 位) + - 普通员工:username 固定为员工手机号(11 位数字) + - Tenant Admin:username 固定为该租户联系人手机号(来源于 public.tenants.contact_phone) 注意:此表位于租户 Schema,username 唯一性约束在 Schema 维度生效。 """ username = models.CharField(max_length=30) @@ -212,7 +213,8 @@ CREATE TABLE login_attempts ( failure_reason VARCHAR(30) CHECK (failure_reason IS NULL OR failure_reason IN ( 'wrong_password','wrong_captcha','account_locked', - 'account_disabled','tenant_not_found' + 'account_disabled','tenant_not_found', + 'wrong_otp','otp_expired' )), PRIMARY KEY (id, attempted_at) -- 分区表主键必须包含分区键 ) PARTITION BY RANGE (attempted_at); @@ -233,6 +235,8 @@ CREATE TABLE login_attempts_default PARTITION OF login_attempts DEFAULT; | `account_locked` | 账号已锁定 | | `account_disabled` | 账号已停用 | | `tenant_not_found` | 租户不存在(理论上不应出现,防御性记录) | +| `wrong_otp` | 短信验证码错误 | +| `otp_expired` | 短信验证码已过期 | #### 索引 @@ -259,6 +263,8 @@ class LoginAttempt(models.Model): ('account_locked', '账号已锁定'), ('account_disabled', '账号已停用'), ('tenant_not_found', '租户不存在'), + ('wrong_otp', '短信验证码错误'), + ('otp_expired', '短信验证码已过期'), ] username = models.CharField(max_length=30) @@ -284,9 +290,9 @@ class LoginAttempt(models.Model): --- -### 3.3 `password_reset_tokens` — 密码重置令牌表(租户 Schema) +### 3.3 `sms_otp_records` — 短信验证码记录表(租户 Schema) -**表说明**:用于通过邮件找回密码的一次性令牌。单次有效,30 分钟过期。 +**表说明**:记录找回密码和手机验证码登录两个场景中,向用户手机号发送的短信验证码及其状态。用 `scene` 字段区分场景(`password_reset` / `login`),有效期和发送频率上限按场景独立控制。原 `password_reset_tokens`(邮件令牌)已废弃,由本表替代。 #### 字段定义 @@ -294,45 +300,61 @@ class LoginAttempt(models.Model): |--------|------|------|--------|------| | `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 | | `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 | -| `token` | `VARCHAR(86)` | `NOT NULL`, `UNIQUE` | — | `secrets.token_urlsafe(64)` 生成(86 字符),全局唯一 | -| `expires_at` | `TIMESTAMPTZ` | `NOT NULL` | — | 过期时间(`created_at + 30 分钟`) | -| `is_used` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否已使用;使用后立即置 True,防止重放攻击 | +| `phone_hash` | `VARCHAR(64)` | `NOT NULL` | — | 收码手机号 SHA-256 哈希(与账号的 `phone_hash` 一致,冗余存储便于限频查询) | +| `scene` | `VARCHAR(20)` | `NOT NULL` | — | 使用场景:`password_reset`(找回密码)或 `login`(验证码登录) | +| `otp_hash` | `VARCHAR(128)` | `NOT NULL` | — | 验证码 PBKDF2 哈希(禁止明文存储,服务端校验时重新哈希比对) | +| `expires_at` | `TIMESTAMPTZ` | `NOT NULL` | — | 过期时间(`password_reset`:`created_at + 10 分钟`;`login`:`created_at + 5 分钟`) | +| `is_used` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否已验证通过;通过后立即置 True,防止重放攻击 | +| `verify_attempts` | `SMALLINT` | `NOT NULL` | `0` | 已尝试验证次数;≥ 5 次后本条记录强制作废(`is_used = TRUE`) | | `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 创建时间 | #### 索引 ```sql -CREATE UNIQUE INDEX uq_password_reset_tokens_token ON password_reset_tokens (token); -CREATE INDEX idx_password_reset_tokens_user ON password_reset_tokens (user_id); -CREATE INDEX idx_password_reset_tokens_expiry ON password_reset_tokens (expires_at) WHERE is_used = FALSE; +CREATE INDEX idx_sms_otp_user ON sms_otp_records (user_id, created_at DESC); +CREATE INDEX idx_sms_otp_phone ON sms_otp_records (phone_hash, created_at DESC); +CREATE INDEX idx_sms_otp_active ON sms_otp_records (user_id, expires_at) WHERE is_used = FALSE; ``` #### Django Model 定义 ```python -class PasswordResetToken(models.Model): +class SmsOtpRecord(models.Model): """ - 密码重置令牌。 + 短信验证码记录(找回密码 + 验证码登录共用)。 安全约束: - - Token 单次有效(is_used=True 后立即失效) - - 有效期 30 分钟 - - 同一账号 1 小时内最多生成 3 个(服务层限频,Redis 计数) + - OTP 明文仅在发送短信时生成,不持久化;仅存 PBKDF2 哈希 + - 单条记录验证次数 ≥ 5 次后强制作废(is_used=True) + - 同一手机号 1 小时内按 scene 独立限频(password_reset ≤ 5 次,login ≤ 10 次) """ - user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='reset_tokens') - token = models.CharField(max_length=86, unique=True) # secrets.token_urlsafe(64) - expires_at = models.DateTimeField() - is_used = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) + SCENE_CHOICES = [ + ('password_reset', '找回密码'), + ('login', '验证码登录'), + ] + + user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='sms_otp_records') + phone_hash = models.CharField(max_length=64) # SHA-256 哈希,不暴露手机号原文 + scene = models.CharField(max_length=20, choices=SCENE_CHOICES) # 区分场景 + otp_hash = models.CharField(max_length=128) # PBKDF2 哈希,禁止明文存储 + expires_at = models.DateTimeField() # password_reset: +10min; login: +5min + is_used = models.BooleanField(default=False) + verify_attempts = models.SmallIntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) class Meta: - db_table = 'password_reset_tokens' + db_table = 'sms_otp_records' indexes = [ - models.Index(fields=['user_id']), + models.Index(fields=['user', '-created_at']), + models.Index(fields=['phone_hash', '-created_at']), ] def is_valid(self) -> bool: from django.utils import timezone - return not self.is_used and timezone.now() < self.expires_at + return not self.is_used and self.verify_attempts < 5 and timezone.now() < self.expires_at + + def mark_used(self): + self.is_used = True + self.save(update_fields=['is_used']) ``` --- @@ -387,8 +409,9 @@ class PasswordHistory(models.Model): | `captcha_token:{uuid}` | STRING | 3 分钟 | 滑块验证会话 Token;验证通过后生成 `captcha_pass_token` | | `captcha_pass:{uuid}` | STRING | 3 分钟 | 一次性通过凭证;登录提交时校验后立即删除 | | `login_fail:{tenant_id}:{username}` | STRING(计数) | 30 分钟 | 连续密码错误次数;≥ 5 触发锁定;TTL 30 分钟自动清零 | -| `recover_email:{email}` | STRING(计数) | 1 小时 | 找回邮件发送次数;上限 3 次/小时 | -| `recover_reset:{account_id}` | STRING(计数) | 1 小时 | 同一账号密码重置 Token 生成次数;上限 3 次/小时 | +| `sms_limit:password_reset:{phone}` | STRING(计数) | 1 小时 | 找回密码场景:同一手机号短信发送次数;上限 **5 次**/小时 | +| `sms_limit:login:{phone}` | STRING(计数) | 1 小时 | 验证码登录场景:同一手机号短信发送次数;上限 **10 次**/小时 | +| `sms_reset_token:{token}` | STRING(user_id) | 15 分钟 | 验证码通过后颁发的一次性重置凭证;用于步骤三提交新密码;使用后立即删除 | | `tenant_verify_ip:{ip}` | STRING(计数) | 1 分钟 | Tenant 验证接口 IP 限流;≥ 10 次拒绝请求 | > **一致性说明**:账号锁定状态由 `user_accounts.status` 持久化,Redis 仅做计数触发器。当 Redis 数据丢失(如 Redis 重启),应用层通过 `locked_until` 字段恢复锁定状态判断。 @@ -435,8 +458,8 @@ class PasswordHistory(models.Model): ### 5.2 账号创建触发时机 | 账号类型 | 触发时机 | 创建者 | username 规则 | 初始密码 | -|----------|----------|--------|--------------|---------| -| Tenant Admin | 平台运营在系统后台开通租户时 | 平台运营 | 自定义(字母开头,6~30 位) | 平台运营自定义 | +|----------|----------|--------|--------------|---------| +| Tenant Admin | 平台运营在系统后台开通租户时 | 平台运营 | **固定为该租户联系人手机号**(11 位数字,来源于 `public.tenants.contact_phone`) | **系统统一固定初始密码**(如 `Fonrey@2025`,由平台部署配置设定) | | 普通员工 | Tenant Admin 在「新增员工」时系统自动生成 | 系统(Tenant Admin 触发) | 固定为员工手机号(11 位) | 系统统一初始密码(部署配置) | --- @@ -472,7 +495,7 @@ org ──► accounts (反向触发,通过 Service 层调用,不通过 FK ``` 0001_initial_user_accounts.py # UserAccount 表(依赖 org.Staff 表已存在) 0002_login_attempts.py # LoginAttempt 表 -0003_password_reset_tokens.py # PasswordResetToken 表 +0003_sms_otp_records.py # SmsOtpRecord 表(替代原 password_reset_tokens) 0004_password_histories.py # PasswordHistory 表 ``` @@ -492,5 +515,6 @@ org ──► accounts (反向触发,通过 Service 层调用,不通过 FK | `phone` 字段拆分为 `phone_enc` + `phone_hash` | 是 | 与 `org.Staff` 保持一致;`phone_enc` 保存原文用于展示,`phone_hash` 用于唯一性校验和快速查询,避免加密字段全表扫描 | | 不扩展 Django `User` | 使用 `AbstractBaseUser` | 避免 `django.contrib.auth.User` 的全局唯一性限制(多租户下同一 username 在不同租户是允许的) | | `LoginAttempt` 不设外键到 `UserAccount` | 是(冗余存储 username) | 即使账号被删除(停用),审计记录仍需保留;使用 username 字符串字段保证审计完整性 | +| 找回密码机制 | 短信验证码(`SmsOtpRecord`) | 经纪人普遍无邮箱;手机号是本系统唯一已知联系方式,短信核验更直接;废弃邮件找回路径,减少外部依赖(SMTP) | | 历史密码单独建表 | `PasswordHistory` 独立表 | 而非在 `UserAccount` 中存 JSON 数组,便于查询和维护,支持未来扩展保留次数 | | 锁定到期时间持久化 | `locked_until` 字段 | Redis 可能重启丢失数据,持久化 `locked_until` 保证 Redis 故障时锁定状态不丢失 | diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md index 03362e37..61b16395 100644 --- a/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md +++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md @@ -877,25 +877,25 @@ _本文档版本 v1.1 | 作者: Backend Architect | 更新时间 2026-04-24_ ```sql -- permission_defs CREATE TABLE permission_defs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(150) NOT NULL UNIQUE, - module VARCHAR(50) NOT NULL, - sub_module VARCHAR(50) NOT NULL DEFAULT '', - group_name VARCHAR(100) NOT NULL, - name VARCHAR(200) NOT NULL, - description TEXT NOT NULL DEFAULT '', - value_type VARCHAR(20) NOT NULL CHECK (value_type IN ('BOOLEAN','SCOPE','INTEGER')), - scope_choices JSONB NOT NULL DEFAULT '[]'::jsonb, - integer_min INTEGER, - integer_max INTEGER, - default_value JSONB NOT NULL DEFAULT '{"v":false}'::jsonb, - max_allowed_categories VARCHAR(50)[] NOT NULL DEFAULT ARRAY[]::VARCHAR[], - sort_order INTEGER NOT NULL DEFAULT 0, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - is_deprecated BOOLEAN NOT NULL DEFAULT FALSE, - version INTEGER NOT NULL DEFAULT 1, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + code VARCHAR(150) NOT NULL UNIQUE, -- 权限码,格式 {module}.{sub_module}.{action}[.{qualifier}],全局唯一,创建后不可修改 + module VARCHAR(50) NOT NULL, -- 一级模块标识,如 property / client / org + sub_module VARCHAR(50) NOT NULL DEFAULT '', -- 二级子模块标识;无子模块时为空字符串 + group_name VARCHAR(100) NOT NULL, -- 权限分组显示名称(管理界面分组展示用) + name VARCHAR(200) NOT NULL, -- 权限项中文名称(管理界面展示) + description TEXT NOT NULL DEFAULT '', -- 权限项说明(管理界面 tooltip 文案) + value_type VARCHAR(20) NOT NULL CHECK (value_type IN ('BOOLEAN','SCOPE','INTEGER')), -- 权限值类型:BOOLEAN=开关 / SCOPE=数据范围 / INTEGER=数量上限 + scope_choices JSONB NOT NULL DEFAULT '[]'::jsonb, -- SCOPE 类型可选范围列表(JSON 数组);非 SCOPE 类型为空数组 + integer_min INTEGER, -- INTEGER 类型最小允许值;其他类型为 NULL + integer_max INTEGER, -- INTEGER 类型最大允许值;其他类型为 NULL + default_value JSONB NOT NULL DEFAULT '{"v":false}'::jsonb, -- 权限默认值,格式 {"v": false/scope_str/int} + max_allowed_categories VARCHAR(50)[] NOT NULL DEFAULT ARRAY[]::VARCHAR[], -- 可配置此权限的角色分类白名单;空数组=无限制 + sort_order INTEGER NOT NULL DEFAULT 0, -- 同分组内排序权重(数值越小越靠前) + is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用;FALSE=已下线,前端配置页隐藏 + is_deprecated BOOLEAN NOT NULL DEFAULT FALSE, -- 是否已废弃;废弃后不可被新角色引用 + version INTEGER NOT NULL DEFAULT 1, -- 乐观锁版本号(每次更新+1) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) CONSTRAINT chk_code_format CHECK (code ~ '^[a-z_]+\.[a-z_]+(\.[a-z_]+){1,2}$') ); CREATE INDEX idx_permission_defs_module ON permission_defs(module, sub_module, sort_order) WHERE is_active = TRUE; @@ -903,18 +903,18 @@ CREATE INDEX idx_permission_defs_active ON permission_defs(is_active) WHERE is_a -- roles CREATE TABLE roles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(100) NOT NULL, - category VARCHAR(30) NOT NULL CHECK (category IN ('agent','store_manager','director','operator','custom')), - description TEXT NOT NULL DEFAULT '', - template_role_id UUID REFERENCES roles(id) ON DELETE SET NULL, - is_system_builtin BOOLEAN NOT NULL DEFAULT FALSE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_by UUID REFERENCES staff(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, - deleted_at TIMESTAMPTZ + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + name VARCHAR(100) NOT NULL, -- 角色显示名称(同租户内唯一,软删除后不参与唯一性校验) + category VARCHAR(30) NOT NULL CHECK (category IN ('agent','store_manager','director','operator','custom')), -- 角色分类:agent=经纪人 / store_manager=门店管理 / director=区域管理 / operator=运营职能 / custom=自定义 + description TEXT NOT NULL DEFAULT '', -- 角色说明文案 + template_role_id UUID REFERENCES roles(id) ON DELETE SET NULL, -- 模板来源(自引用);从某内置角色克隆时记录;NULL=无模板 + is_system_builtin BOOLEAN NOT NULL DEFAULT FALSE, -- 是否系统内置角色;TRUE=不可删除、不可改名 + is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用;FALSE=角色已停用,员工不可再分配此角色 + created_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 创建人(关联 staff 表);系统内置角色为 NULL + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) + updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 最后修改人(关联 staff 表) + deleted_at TIMESTAMPTZ -- 软删除时间戳,NULL=未删除,非NULL=已软删除 ); CREATE UNIQUE INDEX idx_roles_name_active ON roles(name) WHERE deleted_at IS NULL; CREATE INDEX idx_roles_category ON roles(category) WHERE deleted_at IS NULL; @@ -922,13 +922,13 @@ CREATE INDEX idx_roles_template ON roles(template_role_id); -- role_permissions CREATE TABLE role_permissions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, - permission_def_id UUID NOT NULL REFERENCES permission_defs(id) ON DELETE RESTRICT, - value JSONB NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_by UUID REFERENCES staff(id) ON DELETE SET NULL + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, -- 关联角色(角色删除则权限配置同步级联清除) + permission_def_id UUID NOT NULL REFERENCES permission_defs(id) ON DELETE RESTRICT, -- 关联权限定义(有角色引用时权限项不可删除) + value JSONB NOT NULL, -- 权限配置值,格式 {"v": false/scope_str/int},与 permission_defs.value_type 对应 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) + updated_by UUID REFERENCES staff(id) ON DELETE SET NULL -- 最后修改人(关联 staff 表) ); CREATE UNIQUE INDEX idx_role_permissions_uniq ON role_permissions(role_id, permission_def_id); CREATE INDEX idx_role_permissions_role ON role_permissions(role_id); @@ -936,14 +936,14 @@ CREATE INDEX idx_role_permissions_def ON role_permissions(permission_def_id); -- staff_roles CREATE TABLE staff_roles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, - role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, - is_primary BOOLEAN NOT NULL DEFAULT FALSE, - assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - assigned_by UUID REFERENCES staff(id) ON DELETE SET NULL, - valid_from DATE, - valid_until DATE + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, -- 员工 ID(员工删除则角色分配同步级联删除) + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, -- 角色 ID(有员工使用的角色不可删除) + is_primary BOOLEAN NOT NULL DEFAULT FALSE, -- 是否主角色;每员工同时仅可有 1 个主角色(唯一索引保障) + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 角色分配时间(系统自动) + assigned_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 分配操作人(关联 staff 表);NULL=系统自动分配 + valid_from DATE, -- 角色有效期开始日期;NULL=立即生效 + valid_until DATE -- 角色有效期结束日期;NULL=永久有效 ); CREATE UNIQUE INDEX idx_staff_roles_uniq ON staff_roles(staff_id, role_id); CREATE UNIQUE INDEX idx_staff_roles_primary ON staff_roles(staff_id) WHERE is_primary = TRUE; @@ -951,32 +951,32 @@ CREATE INDEX idx_staff_roles_role ON staff_roles(role_id); -- staff_permission_overrides CREATE TABLE staff_permission_overrides ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, - permission_def_id UUID NOT NULL REFERENCES permission_defs(id) ON DELETE RESTRICT, - value JSONB NOT NULL, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, -- 员工 ID(员工删除则个人覆盖配置同步级联删除) + permission_def_id UUID NOT NULL REFERENCES permission_defs(id) ON DELETE RESTRICT, -- 关联权限定义 + value JSONB NOT NULL, -- 覆盖配置值,格式与 role_permissions.value 一致 override_mode VARCHAR(10) NOT NULL DEFAULT 'REPLACE' - CHECK (override_mode IN ('REPLACE','RESTRICT','GRANT')), - reason TEXT NOT NULL DEFAULT '', - modified_by UUID REFERENCES staff(id) ON DELETE SET NULL, - modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + CHECK (override_mode IN ('REPLACE','RESTRICT','GRANT')), -- 覆盖模式:REPLACE=完全替换角色权限 / RESTRICT=向下收紧 / GRANT=向上提升 + reason TEXT NOT NULL DEFAULT '', -- 覆盖理由(操作审计留存) + modified_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 修改操作人(关联 staff 表) + modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- 修改时间(系统自动) ); CREATE UNIQUE INDEX idx_staff_overrides_uniq ON staff_permission_overrides(staff_id, permission_def_id); CREATE INDEX idx_staff_overrides_staff ON staff_permission_overrides(staff_id); -- staff_data_scopes CREATE TABLE staff_data_scopes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, - scope_type VARCHAR(20) NOT NULL + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, -- 员工 ID + scope_type VARCHAR(20) NOT NULL -- 数据范围类型:self=本人 / group=小组 / store=门店 / area=大区 / region=区域 / company=全公司 / custom_unit=自定义单元 CHECK (scope_type IN ('self','group','store','area','region','company','custom_unit')), - org_unit_id UUID REFERENCES org_units(id) ON DELETE RESTRICT, - is_readable BOOLEAN NOT NULL DEFAULT TRUE, - is_writable BOOLEAN NOT NULL DEFAULT FALSE, - granted_by UUID REFERENCES staff(id) ON DELETE SET NULL, - granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - expires_at TIMESTAMPTZ, - reason TEXT NOT NULL DEFAULT '', + org_unit_id UUID REFERENCES org_units(id) ON DELETE RESTRICT, -- 自定义组织单元;scope_type=custom_unit 时必填,其他为 NULL + is_readable BOOLEAN NOT NULL DEFAULT TRUE, -- 是否有读权限 + is_writable BOOLEAN NOT NULL DEFAULT FALSE, -- 是否有写权限 + granted_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 授权操作人(关联 staff 表) + granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 授权时间(系统自动) + expires_at TIMESTAMPTZ, -- 到期时间;NULL=永久有效 + reason TEXT NOT NULL DEFAULT '', -- 数据范围授权理由(操作审计留存) CONSTRAINT chk_custom_unit_has_org CHECK ( (scope_type = 'custom_unit' AND org_unit_id IS NOT NULL) OR (scope_type <> 'custom_unit') @@ -988,22 +988,22 @@ CREATE INDEX idx_data_scopes_expires ON staff_data_scopes(expires_at) WHERE expi -- permission_change_logs (append-only, no deleted_at) CREATE TABLE permission_change_logs ( - id UUID NOT NULL DEFAULT gen_random_uuid(), + id UUID NOT NULL DEFAULT gen_random_uuid(), -- 主键(与 operated_at 组成复合主键,分区表要求) operated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键(原 operated_at 前置) - target_type VARCHAR(30) NOT NULL + target_type VARCHAR(30) NOT NULL -- 操作对象类型:role / role_permission / staff_role / staff_override / staff_scope CHECK (target_type IN ('role','role_permission','staff_role','staff_override','staff_scope')), - target_id UUID NOT NULL, - staff_id UUID REFERENCES staff(id) ON DELETE SET NULL, - role_id UUID REFERENCES roles(id) ON DELETE SET NULL, - permission_code VARCHAR(150), - action VARCHAR(20) NOT NULL + target_id UUID NOT NULL, -- 操作对象 ID + staff_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 被操作员工 ID(如分配/撤销角色时的目标员工) + role_id UUID REFERENCES roles(id) ON DELETE SET NULL, -- 被操作角色 ID + permission_code VARCHAR(150), -- 操作涉及的权限码(冗余存储,避免关联查询) + action VARCHAR(20) NOT NULL -- 操作类型:create=新建 / update=修改 / delete=删除 / assign=分配 / revoke=撤销 CHECK (action IN ('create','update','delete','assign','revoke')), - old_value JSONB, - new_value JSONB, - operator_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, - operator_ip INET, - user_agent TEXT, - reason TEXT NOT NULL DEFAULT '', + old_value JSONB, -- 变更前值;create 时为 NULL + new_value JSONB, -- 变更后值;delete 时为 NULL + operator_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, -- 操作人员工 ID(RESTRICT:操作记录保留,操作人不可删除) + operator_ip INET, -- 操作人来源 IP + user_agent TEXT, -- 操作人客户端 UA + reason TEXT NOT NULL DEFAULT '', -- 操作理由(可选,审计留存) PRIMARY KEY (id, operated_at) -- 分区表主键必须包含分区键 ) PARTITION BY RANGE (operated_at); diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md index d1c248a8..8eece44b 100644 --- a/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md +++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md @@ -2,8 +2,8 @@ # Fonrey — Public Schema 数据模型 > **作者**: Backend Architect -> **版本**: v1.2 -> **日期**: 2026-04-26 +> **版本**: v1.3 +> **日期**: 2026-04-30 > **权威源**: 本文件是 `public` schema 所有表的唯一权威定义 > **设计依据**: 系统管理模块 PRD(`PRD/系统管理/系统管理模块PRD.md`);客户端发布管理模块 PRD(`PRD/发布管理/客户端发布管理模块PRD.md`) > **索引文档**: [`DATA_MODEL.md §三`](./DATA_MODEL.md)(仅保留摘要索引,开发以本文件为准) @@ -87,11 +87,13 @@ PostgreSQL Instance -- 租户主表(每家房产经纪公司一条记录) CREATE TABLE public.tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - schema_name VARCHAR(63) UNIQUE NOT NULL, -- PG schema 名,最长 63 字符,创建后不可修改 + schema_name VARCHAR(63) UNIQUE NOT NULL, -- PG schema 名,最长 63 字符,创建后不可修改;命名规则:t_{uuid前8位},如 t_3f2a1b4c + tenant_code CHAR(12) UNIQUE NOT NULL, -- 对外暴露的 12 位纯数字识别码,如 202500010001;用户登录时输入,由平台运营在注册时生成;创建后不可修改 name VARCHAR(255) NOT NULL, -- 公司名称 short_name VARCHAR(100), -- 简称/品牌名 contact_name VARCHAR(100) NOT NULL, -- 主联系人姓名 - contact_email VARCHAR(254) NOT NULL, -- 联系邮箱(接收通知/欢迎邮件) + contact_phone CHAR(11) NOT NULL, -- 联系人手机号(11位纯数字);系统开通租户时自动以此手机号创建 Tenant Admin 登录账号;必填 + contact_email VARCHAR(254), -- 联系邮箱(接收通知/欢迎邮件;选填,为空时 Tenant Admin 无法自助找回密码) region VARCHAR(100), -- 所在地区(省市,如「上海市」) plan VARCHAR(20) NOT NULL DEFAULT 'basic' CHECK (plan IN ('basic','professional','enterprise')), @@ -857,6 +859,7 @@ ORDER BY created_at DESC; | `UPDATE public.tenant_status_logs` | append-only 审计表 | | `DELETE FROM public.platform_audit_logs` | append-only 审计表 | | `UPDATE public.tenants SET schema_name = ...` | schema 名绑定 PG 物理 schema | +| `UPDATE public.tenants SET tenant_code = ...` | 12 位识别码一旦发放给用户不可更改,避免经纪人无法登录 | | `UPDATE public.domains SET domain = ...` | 域名路由不可变 | | `UPDATE public.client_releases SET version = ...` | 版本号创建后不可修改,变更须新建记录 | | 在租户 schema 中创建 `client_releases` 副本 | 本表属于 public schema,多租户共享,禁止下沉到租户层 | diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_SETTING.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_SETTING.md index 669173f0..cfa63f6e 100644 --- a/Project/fonrey/DATA_MODEL/DATA_MODEL_SETTING.md +++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_SETTING.md @@ -50,14 +50,14 @@ apps/setting/ -- 研发预置,租户不可修改分组本身 -- ============================================================ CREATE TABLE lookup_groups ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) module VARCHAR(50) NOT NULL, -- 'client' | 'property' key VARCHAR(100) NOT NULL, -- 'source' | 'follow_purpose' label_zh VARCHAR(50) NOT NULL, -- 界面显示名称,如「客源来源」 description TEXT, -- 说明文案(前端 tooltip 使用) - sort_order SMALLINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + sort_order SMALLINT NOT NULL DEFAULT 0, -- 排序权重(数值越小越靠前) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) UNIQUE (module, key) ); ``` @@ -80,16 +80,16 @@ CREATE TABLE lookup_groups ( -- 租户管理员可增删排序;is_system=True 的预制项不可物理删除 -- ============================================================ CREATE TABLE lookup_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - group_id UUID NOT NULL REFERENCES lookup_groups(id) ON DELETE CASCADE, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) + group_id UUID NOT NULL REFERENCES lookup_groups(id) ON DELETE CASCADE, -- 所属枚举分组(关联 lookup_groups) value VARCHAR(100) NOT NULL, -- 存储值,英文 snake_case(如 'door_to_door') label_zh VARCHAR(50) NOT NULL, -- 显示文本(如「上门」) is_system BOOLEAN NOT NULL DEFAULT FALSE, -- True=系统预制,不可删除,仅可停用 - is_active BOOLEAN NOT NULL DEFAULT TRUE, - sort_order SMALLINT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用;FALSE 后前端下拉不展示,历史数据保留并追加「(已停用)」后缀 + sort_order SMALLINT NOT NULL DEFAULT 0, -- 排序权重(数值越小越靠前) created_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 系统预制时为 NULL - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) UNIQUE (group_id, value) ); @@ -153,14 +153,14 @@ CREATE INDEX idx_lookup_items_group_active -- 租户标量配置(键值对)(Tenant Schema) -- ============================================================ CREATE TABLE tenant_settings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) category VARCHAR(50) NOT NULL, -- 配置分类:'client' | 'property' | 'showroom' key VARCHAR(100) NOT NULL, -- 配置 key,如 'duplicate_check_scope' value JSONB NOT NULL, -- 存储任意类型(bool/int/str),如 {"v": "self"} value_type VARCHAR(20) NOT NULL -- 'bool' | 'int' | 'string' | 'enum'(用于前端渲染控件) CHECK (value_type IN ('bool', 'int', 'string', 'enum')), - updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 最后修改人(关联 staff 表) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) UNIQUE (category, key) ); @@ -193,7 +193,7 @@ CREATE INDEX idx_tenant_settings_category ON tenant_settings(category); -- MVP 仅支持 module='property' -- ============================================================ CREATE TABLE field_requirement_rules ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关) module VARCHAR(20) NOT NULL, -- 'property' | 'client'(MVP 只用 'property') entity_type VARCHAR(50) NOT NULL, -- 与 property.property_type CHECK 约束值对齐 -- 'residential'|'villa'|'commercial_residential'|'shop'|'office'|'other' @@ -202,8 +202,8 @@ CREATE TABLE field_requirement_rules ( field_key VARCHAR(50) NOT NULL, -- 字段 key,如 'orientation'|'decoration'|'floor' requirement VARCHAR(10) NOT NULL -- 规则值 CHECK (requirement IN ('required', 'optional', 'hidden')), - updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 最后修改人(关联 staff 表) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动) UNIQUE (module, entity_type, trade_status, field_key) ); diff --git a/Project/fonrey/DATA_MODEL/SCHEMA_CHANGES.md b/Project/fonrey/DATA_MODEL/SCHEMA_CHANGES.md new file mode 100644 index 00000000..ef9cd550 --- /dev/null +++ b/Project/fonrey/DATA_MODEL/SCHEMA_CHANGES.md @@ -0,0 +1,111 @@ +# Schema 变更记录 + +> **用途**:记录在初始 Model 创建之后新增/修改的字段,供架构师逐一更新 Migration 脚本。 +> **格式**:每条变更含目标表、变更类型、字段定义、变更原因、来源 PRD 依据。 +> **状态流转**:`待迁移` → `已迁移`(架构师完成 migration 后更新状态) + +--- + +## 变更清单 + +| # | 状态 | 目标表 | 变更类型 | 字段名 | 提出日期 | +|---|------|--------|----------|--------|----------| +| 1 | 待迁移 | `public.tenants` | 新增字段 | `tenant_code` | 2026-04-30 | +| 2 | 待迁移 | `public.tenants` | 新增字段 | `contact_phone` | 2026-04-30 | +| 3 | 待迁移 | `public.tenants` | 字段修改 | `contact_email`(NOT NULL → 可 NULL) | 2026-04-30 | + +--- + +## 变更详情 + +### #1 — `public.tenants` 新增 `tenant_code` + +**状态**:待迁移 +**提出日期**:2026-04-30 +**目标表**:`public.tenants`(Public Schema,django-tenants 主租户表) + +**字段定义**: +```sql +tenant_code CHAR(12) UNIQUE NOT NULL +``` + +**注释**:对外暴露的 12 位纯数字识别码,如 `202500010001`;用户首次登录客户端时输入;由平台运营在注册租户时生成;创建后不可修改。 + +**推荐加在**:`schema_name` 字段之后,`name` 字段之前。 + +**约束要求**: +- `CHAR(12)`:固定 12 位 +- `UNIQUE`:全局唯一 +- `NOT NULL`:注册时必填 +- 禁止 `UPDATE`(应用层 + DB 层双重约束,与 `schema_name` 同等级别) + +**建议索引**: +```sql +CREATE UNIQUE INDEX idx_tenants_tenant_code ON public.tenants(tenant_code); +``` + +**变更原因**: +登录 PRD §5.1.3 明确用户输入 12 位 Tenant Code 完成租户识别,服务端需通过该码查找对应 `schema_name`,原始 `public.tenants` 表中缺少此字段。 + +**PRD 依据**: +`PRD/登录管理/用户登录管理模块PRD.md` §5.1.3 — Tenant Code 格式规范: +> 格式:固定 12 位纯数字,如 `202500010001` + +**登录查询示例**: +```sql +SELECT schema_name, name +FROM public.tenants +WHERE tenant_code = '202500010001' + AND status = 'active'; +``` + +--- + +### #2 — `public.tenants` 新增 `contact_phone` + +**状态**:待迁移 +**提出日期**:2026-04-30 +**目标表**:`public.tenants`(Public Schema) + +**字段定义**: +```sql +contact_phone CHAR(11) NOT NULL +``` + +**注释**:联系人手机号(11位纯数字);系统开通租户时自动以此手机号创建 Tenant Admin 登录账号;必填字段,平台运营在创建租户时录入。 + +**推荐加在**:`contact_name` 字段之后,`contact_email` 字段之前。 + +**约束要求**: +- `CHAR(11)`:固定 11 位手机号 +- `NOT NULL`:开通租户时必填 + +**变更原因**: +登录 PRD v1.5 决策:Tenant Admin 账号统一以联系人手机号作为用户名,与普通员工账号规则对齐。需在 tenants 表增加手机号字段作为 Tenant Admin 账号创建的数据来源。 + +**PRD 依据**: +`PRD/登录管理/用户登录管理模块PRD.md` §5.3.2 Tenant Admin 账号规格表 + +--- + +### #3 — `public.tenants.contact_email` 改为可 NULL + +**状态**:待迁移 +**提出日期**:2026-04-30 +**目标表**:`public.tenants`(Public Schema) + +**变更 SQL**: +```sql +ALTER TABLE public.tenants + ALTER COLUMN contact_email DROP NOT NULL; +``` + +**变更原因**: +`contact_email` 原为 `NOT NULL`。由于 Tenant Admin 账号创建数据来源改为 `contact_phone`(手机号,必填),邮箱降级为选填,仅用于找回密码功能。为空时 Tenant Admin 无法自助找回密码,需联系平台运营处理。 + +**PRD 依据**: +`PRD/登录管理/用户登录管理模块PRD.md` §5.3.2 + +--- + +*后续如有新增字段,按同样格式追加到本文件。* diff --git a/Project/fonrey/PRD/PERSONA_定义.md b/Project/fonrey/PRD/PERSONA_定义.md new file mode 100644 index 00000000..b0e7418a --- /dev/null +++ b/Project/fonrey/PRD/PERSONA_定义.md @@ -0,0 +1,177 @@ +# Fonrey 系统角色 Persona 定义 + +**版本**:v1.0 +**状态**:已定稿 +**作者**:产品团队 +**最后更新**:2026-04-30 + +> 本文档是 Fonrey 所有 PRD、DATA_MODEL、PERMISSION_SEED 等文档的角色命名**唯一权威来源**。 +> 所有其他文档中的角色称谓**必须**以本文档为准,禁止自造名称或混用。 + +--- + +## 一、角色层级概览 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Fonrey SaaS 平台 │ +│ │ +│ ① Platform Admin(平台超级管理员) │ +│ └── 管理所有租户:开通、暂停、配置、版本升级 │ +│ │ +│ ─ ─ ─ ─ ─ ─ ─ 租 户 边 界 ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +│ │ +│ ② Tenant Admin(租户管理员) │ +│ └── 管理本租户:组织架构、账号、权限、系统配置 │ +│ │ +│ ③ Agent(经纪人) │ +│ └── 日常业务操作:录入房源、跟进客源、查看数据 │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、Persona 详细定义 + +### P1 — Platform Admin(平台超级管理员) + +| 属性 | 值 | +| ---------------- | --------------------- | +| **Persona 名称** | Platform Admin | +| **中文称谓** | 平台超级管理员 | +| **Persona Code** | `PLATFORM_ADMIN` | +| **所属层** | 平台层(Platform) | +| **账号归属** | 平台运营团队(Fonrey 公司内部人员) | +| **账号数量** | 极少,手动创建,不通过租户系统管理 | +| **认证入口** | 独立管理后台(非租户客户端) | +| **Schema 归属** | `public`(跨所有租户) | + +**职责范围:** +- 创建、暂停、注销租户(Tenant) +- 为租户初始化 Tenant Admin 账号 +- 管理平台版本、发布客户端安装包 +- 监控系统健康状态、查看平台级日志 +- 配置平台级参数(短信网关、OSS、第三方集成等) + +**不涉及:** +- 不进入任何租户的业务数据 +- 不参与租户内部的权限分配、组织管理 + +**关键约束:** +- Platform Admin 走**独立认证体系**,不纳入租户 RBAC 权限模型 +- 无需在 `permission_definitions` 中注册权限项 + +--- + +### P2 — Tenant Admin(租户管理员) + +| 属性 | 值 | +| ---------------- | ---------------------------------- | +| **Persona 名称** | Tenant Admin | +| **中文称谓** | 租户管理员 | +| **Persona Code** | `TENANT_ADMIN` | +| **所属层** | 租户层(Tenant) | +| **账号归属** | 各租户(房产经纪公司)的系统管理员 | +| **账号数量** | 每个租户 1~3 个,由 Platform Admin 创建初始账号 | +| **认证入口** | 与经纪人相同的租户客户端(Electron App) | +| **登录账号** | 平台运营分配的自定义字符串(不限于手机号格式) | +| **初始密码** | 平台统一固定初始密码,首次登录强制修改 | +| **Schema 归属** | 租户 Schema(`tenant_{id}`) | +| | | + +**职责范围:** +- 维护组织架构(部门/门店树) +- 办理员工入职、离职、调动 +- 创建和管理员工系统账号 +- 配置角色与权限(角色创建、权限分配、个人权限调整) +- 配置系统枚举值(Lookup Items)、房源录入规则、客源规则 +- 查看全租户范围的业务数据(受数据权限规则约束) + +**不涉及:** +- 不能跨租户操作 +- 不能修改平台级配置(版本、短信网关等) + +**与「经纪人」的区别:** +- Tenant Admin 是**管理身份**,不从事日常房源/客源业务操作 +- 在系统内显示为「管理员」身份,拥有全模块管理权限 +- 可以同时持有某个业务角色(如总经),但账号性质以 Tenant Admin 为主 + +**在 PRD 中的出现场景:** +- 组织人事管理模块的主操作者 +- 权限管理模块的主操作者 +- 系统配置模块的主操作者 +- 在楼盘管理、发布管理中执行管理操作的用户 + +--- + +### P3 — Agent(经纪人) + +| 属性 | 值 | +| ---------------- | ----------------------------- | +| **Persona 名称** | Agent | +| **中文称谓** | 经纪人 | +| **Persona Code** | `AGENT` | +| **所属层** | 租户层(Tenant) | +| **账号归属** | 各租户的在职员工 | +| **账号数量** | 每个租户 N 个,由 Tenant Admin 创建 | +| **认证入口** | 租户客户端(Electron App) | +| **登录账号** | 手机号(由 Tenant Admin 录入员工时自动创建) | +| **初始密码** | 系统统一固定初始密码,首次登录强制修改 | +| **Schema 归属** | 租户 Schema(`tenant_{id}`) | + +**内部岗位子类型(Agent Sub-roles):** + +Agent 是统称,内部通过「角色」区分岗位权限层级。系统内置以下角色(不可删除): + +| 角色名称 | Role Code | 典型岗位 | 数据权限范围 | +| ---- | ---------------- | -------- | --------- | +| 置业顾问 | `ROLE_AGENT` | 一线经纪人 | 仅自己的数据 | +| 店管 | `ROLE_STORE_MGR` | 门店店长 | 本门店数据 | +| 区管 | `ROLE_AREA_MGR` | 区域经理 | 本区域数据 | +| 区总 | `ROLE_AREA_DIR` | 区域总监 | 本区域+下属区数据 | +| 副总 | `ROLE_VP` | 副总经理 | 全公司数据(部分) | +| 总经 | `ROLE_GM` | 总经理 | 全公司数据 | +| 其他职能 | `ROLE_OTHER` | 行政/财务/HR | 按需配置 | + +**在 PRD 中的出现场景:** +- 房源管理模块的主操作者 +- 客源管理模块的主操作者 +- 楼盘管理模块的查看用户 +- 登录模块的主要使用者 + +--- + +## 三、命名规范与替换对照表 + +以下为历史文档中出现的混乱称谓,与标准称谓的对照关系: + +| 历史称谓(禁止继续使用) | 应替换为 | 说明 | +| ------------ | ----------------------- | ---------------- | +| 超级管理员 | Platform Admin(平台超级管理员) | 仅指平台层 | +| 平台管理员 | Platform Admin(平台超级管理员) | 同上 | +| 系统管理员 | Tenant Admin(租户管理员) | 租户层管理员 | +| 管理员(泛称) | Tenant Admin(租户管理员) | 明确指向租户层 | +| HR 管理员 | Tenant Admin(租户管理员) | 同一人,不区分子角色 | +| HR 行政 | Tenant Admin(租户管理员) | 同上 | +| 一线经纪人 | Agent(经纪人) | 统称,角色层面才区分岗位 | +| 置业顾问(作为用户称谓) | Agent(经纪人) | 置业顾问仅作为「内置角色名」使用 | + +**规则说明:** +1. PRD User Story 的 **As a** 部分:使用中文称谓(`Tenant Admin(租户管理员)`、`Agent(经纪人)`) +2. 权限矩阵、DATA_MODEL 注释:使用 Persona Code(`TENANT_ADMIN`、`AGENT`) +3. 角色名称(置业顾问、店管、总经等)**仅在角色管理相关语境中**出现,不作为用户身份称谓 +4. `Platform Admin` 在各 PRD 中尽量少出现,其功能属于「系统管理模块」,不在各子模块 PRD 内展开 + +--- + +## 四、快速索引 + +| 场景 | 使用 | +|------|------| +| PRD User Story 主语 | `Tenant Admin(租户管理员)` / `Agent(经纪人)` | +| 错误提示文案(面向用户) | 「请联系您的租户管理员」 | +| DATA_MODEL 注释 | `created_by: 创建该记录的 Agent(经纪人)用户 ID` | +| 权限矩阵行标题 | 使用角色名(置业顾问 / 店管 / 总经 ...) | +| 代码注释 / 枚举值 | `PLATFORM_ADMIN` / `TENANT_ADMIN` / `AGENT` | +| 日志、审计记录 | `operator_type: PLATFORM_ADMIN / TENANT_ADMIN / AGENT` | diff --git a/Project/fonrey/PRD/PRD_MVP.md b/Project/fonrey/PRD/PRD_MVP.md index a8bde2e5..a1b859ee 100644 --- a/Project/fonrey/PRD/PRD_MVP.md +++ b/Project/fonrey/PRD/PRD_MVP.md @@ -180,7 +180,7 @@ ## 4. 用户故事(MVP 核心路径) ### Story 1 — 经纪人录入房源 -> As a **一线经纪人**, +> As a **Agent(经纪人)**, > I want to **快速录入一套二手住宅并上传图片和业主联系方式**, > So that **这套房源的信息能被团队所有成员找到和跟进**. @@ -192,7 +192,7 @@ --- ### Story 2 — 经纪人跟进房源 -> As a **一线经纪人**, +> As a **Agent(经纪人)**, > I want to **对我负责的房源记录每次跟进(面访/电话/钥匙/实勘)**, > So that **我的跟进历史有据可查,团队不会重复联系同一业主**. @@ -204,7 +204,7 @@ --- ### Story 3 — 经纪人录入客源 -> As a **一线经纪人**, +> As a **Agent(经纪人)**, > I want to **录入意向购房/租房客户并跟进其需求变化**, > So that **我能在合适时机将客户与合适房源匹配**. @@ -216,7 +216,7 @@ --- ### Story 4 — 转成交 -> As a **一线经纪人**, +> As a **Agent(经纪人)**, > I want to **将已达成交易的客源标记为"成交"并关联成交房源**, > So that **成交数据进入系统留存,房源状态自动更新**. diff --git a/Project/fonrey/PRD/TASK.md b/Project/fonrey/PRD/TASK.md index feea5b4e..e8cdd21e 100644 --- a/Project/fonrey/PRD/TASK.md +++ b/Project/fonrey/PRD/TASK.md @@ -88,8 +88,8 @@ | [US-SETTING-010](#US-SETTING-010-管理员配置首页展示内容) | 系统配置 | 管理员配置首页展示内容 | [ ] | | [US-SETTING-011](#US-SETTING-011-管理员配置相关方规则) | 系统配置 | 管理员配置相关方规则 | [ ] | | [US-SETTING-012](#US-SETTING-012-管理员配置客源相关参数) | 系统配置 | 管理员配置客源相关参数 | [ ] | -| [US-SYSTEM-010](#US-SYSTEM-010-平台管理员管理租户开通暂停配置) | 系统管理 | 平台管理员管理租户(开通/暂停/配置) | [ ] | -| [US-SYSTEM-011](#US-SYSTEM-011-平台管理员监控系统健康状态) | 系统管理 | 平台管理员监控系统健康状态 | [ ] | +| [US-SYSTEM-010](#US-SYSTEM-010-Platform Admin(平台超级管理员)管理租户开通暂停配置) | 系统管理 | Platform Admin(平台超级管理员)管理租户(开通/暂停/配置) | [ ] | +| [US-SYSTEM-011](#US-SYSTEM-011-Platform Admin(平台超级管理员)监控系统健康状态) | 系统管理 | Platform Admin(平台超级管理员)监控系统健康状态 | [ ] | | [US-RELEASE-010](#US-RELEASE-010-系统发布Windows桌面客户端安装包) | 客户端发布 | 系统发布Windows桌面客户端安装包 | [ ] | | [US-RELEASE-011](#US-RELEASE-011-客户端自动检测并更新至最新版本) | 客户端发布 | 客户端自动检测并更新至最新版本 | [ ] | @@ -110,8 +110,8 @@ | [US-SETTING-021](#US-SETTING-021-管理员配置交易规则) | 系统配置 | 管理员配置交易规则 | [ ] | | ~~US-SETTING-022~~ | ~~系统配置~~ | ~~管理员配置财务规则~~ — **已移出,财务模块 Out of Scope** | ~~[ ]~~ | | ~~US-SETTING-023~~ | ~~系统配置~~ | ~~管理员配置合同模板~~ — **已移出,合同模块 Out of Scope** | ~~[ ]~~ | -| [US-SYSTEM-020](#US-SYSTEM-020-平台管理员查看操作审计日志) | 系统管理 | 平台管理员查看操作审计日志 | [ ] | -| [US-SYSTEM-021](#US-SYSTEM-021-平台管理员管理灰度发布滚动升级) | 系统管理 | 平台管理员管理灰度发布/滚动升级 | [ ] | +| [US-SYSTEM-020](#US-SYSTEM-020-Platform Admin(平台超级管理员)查看操作审计日志) | 系统管理 | Platform Admin(平台超级管理员)查看操作审计日志 | [ ] | +| [US-SYSTEM-021](#US-SYSTEM-021-Platform Admin(平台超级管理员)管理灰度发布滚动升级) | 系统管理 | Platform Admin(平台超级管理员)管理灰度发布/滚动升级 | [ ] | --- @@ -682,13 +682,13 @@ ### 系统管理(运营后台) -##### US-SYSTEM-010 平台管理员管理租户开通暂停配置 +##### US-SYSTEM-010 Platform Admin(平台超级管理员)管理租户开通暂停配置 - 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 租户管理(开通/暂停/配置) - 状态:[ ] - 验收标准:可在运营后台新开通租户(自动创建独立PostgreSQL Schema);可暂停租户(暂停后租户用户无法登录);可为租户配置域名/子域名;租户操作记录写入平台操作日志 -##### US-SYSTEM-011 平台管理员监控系统健康状态 +##### US-SYSTEM-011 Platform Admin(平台超级管理员)监控系统健康状态 - 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 系统健康监控 - 状态:[ ] @@ -818,13 +818,13 @@ ### 系统管理(运营后台) -##### US-SYSTEM-020 平台管理员查看操作审计日志 +##### US-SYSTEM-020 Platform Admin(平台超级管理员)查看操作审计日志 - 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 操作审计日志 - 状态:[ ] - 验收标准:(规划中,详细验收标准待PRD细化后补充) -##### US-SYSTEM-021 平台管理员管理灰度发布滚动升级 +##### US-SYSTEM-021 Platform Admin(平台超级管理员)管理灰度发布滚动升级 - 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 灰度发布/滚动升级 - 状态:[ ] diff --git a/Project/fonrey/PRD/TASK_AGENT_READY.md b/Project/fonrey/PRD/TASK_AGENT_READY.md index 1523d140..bd6e64b9 100644 --- a/Project/fonrey/PRD/TASK_AGENT_READY.md +++ b/Project/fonrey/PRD/TASK_AGENT_READY.md @@ -5489,7 +5489,7 @@ Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天 - 未完成项/阻塞项 ``` -### US-SYSTEM-010 平台管理员管理租户开通暂停配置 +### US-SYSTEM-010 Platform Admin(平台超级管理员)管理租户开通暂停配置 - 阶段:**P1** - 模块:**系统管理(运营后台)** @@ -5508,7 +5508,7 @@ Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天 你是 OpenCode 编程代理。请在当前仓库根目录完成下面任务。 【任务ID】US-SYSTEM-010 -【任务标题】平台管理员管理租户开通暂停配置 +【任务标题】Platform Admin(平台超级管理员)管理租户开通暂停配置 【阶段】P1 【模块】系统管理(运营后台) @@ -5563,7 +5563,7 @@ Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天 - 未完成项/阻塞项 ``` -### US-SYSTEM-011 平台管理员监控系统健康状态 +### US-SYSTEM-011 Platform Admin(平台超级管理员)监控系统健康状态 - 阶段:**P1** - 模块:**系统管理(运营后台)** @@ -5582,7 +5582,7 @@ Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天 你是 OpenCode 编程代理。请在当前仓库根目录完成下面任务。 【任务ID】US-SYSTEM-011 -【任务标题】平台管理员监控系统健康状态 +【任务标题】Platform Admin(平台超级管理员)监控系统健康状态 【阶段】P1 【模块】系统管理(运营后台) @@ -6651,7 +6651,7 @@ electron-builder 输出 NSIS .exe 安装包和便携版 .zip;安装包经EV证 - 未完成项/阻塞项 ``` -### US-SYSTEM-020 平台管理员查看操作审计日志 +### US-SYSTEM-020 Platform Admin(平台超级管理员)查看操作审计日志 - 阶段:**P2** - 模块:**系统管理(运营后台)** @@ -6671,7 +6671,7 @@ electron-builder 输出 NSIS .exe 安装包和便携版 .zip;安装包经EV证 你是 OpenCode 编程代理。请在当前仓库根目录完成下面任务。 【任务ID】US-SYSTEM-020 -【任务标题】平台管理员查看操作审计日志 +【任务标题】Platform Admin(平台超级管理员)查看操作审计日志 【阶段】P2 【模块】系统管理(运营后台) @@ -6725,7 +6725,7 @@ electron-builder 输出 NSIS .exe 安装包和便携版 .zip;安装包经EV证 - 未完成项/阻塞项 ``` -### US-SYSTEM-021 平台管理员管理灰度发布滚动升级 +### US-SYSTEM-021 Platform Admin(平台超级管理员)管理灰度发布滚动升级 - 阶段:**P2** - 模块:**系统管理(运营后台)** @@ -6745,7 +6745,7 @@ electron-builder 输出 NSIS .exe 安装包和便携版 .zip;安装包经EV证 你是 OpenCode 编程代理。请在当前仓库根目录完成下面任务。 【任务ID】US-SYSTEM-021 -【任务标题】平台管理员管理灰度发布滚动升级 +【任务标题】Platform Admin(平台超级管理员)管理灰度发布滚动升级 【阶段】P2 【模块】系统管理(运营后台) diff --git a/Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md b/Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md index 5418db37..29ba0dbb 100644 --- a/Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md +++ b/Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md @@ -5,7 +5,7 @@ **版本**: 1.0 **所属系统**: Fonrey 房产经纪管理系统 **关联模块**: 系统管理、权限管理 -**干系人**: 工程负责人、运维负责人、系统管理员 +**干系人**: 工程负责人、运维负责人、Tenant Admin(租户管理员) --- @@ -24,9 +24,9 @@ Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通 | 角色 | 使用场景 | 使用频率 | |------|---------|----------| -| 一线经纪人 | 下载安装客户端、日常登录使用系统、接受自动更新 | 每日 | +| Agent(经纪人) | 下载安装客户端、日常登录使用系统、接受自动更新 | 每日 | | 店长/经理 | 同上 | 每日 | -| 系统管理员 | 发布新版本、管理安装包下载地址、监控客户端版本分布 | 按需 | +| Tenant Admin(租户管理员) | 发布新版本、管理安装包下载地址、监控客户端版本分布 | 按需 | | IT 运维人员 | 维护更新服务器、签名证书、构建发布流水线 | 按发布周期 | ### 核心痛点 @@ -65,7 +65,7 @@ Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通 ### Story 1:经纪人下载并安装客户端 -**As** 一线经纪人,**I want** 通过公司提供的网址下载一个安装程序并完成安装,**So that** 我可以立即打开登录界面使用 Fonrey 系统,无需手动配置浏览器。 +**As** Agent(经纪人),**I want** 通过公司提供的网址下载一个安装程序并完成安装,**So that** 我可以立即打开登录界面使用 Fonrey 系统,无需手动配置浏览器。 **验收标准**: - [ ] 官方下载页面可通过指定 URL 访问,页面展示最新版本号、发布日期及下载按钮 @@ -80,7 +80,7 @@ Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通 ### Story 2:经纪人使用客户端正常登录并使用系统 -**As** 一线经纪人,**I want** 打开客户端后直接访问 Fonrey 系统的完整功能,**So that** 我的日常使用体验与使用 Chrome 浏览器无差异,且不受本机安装的浏览器版本影响。 +**As** Agent(经纪人),**I want** 打开客户端后直接访问 Fonrey 系统的完整功能,**So that** 我的日常使用体验与使用 Chrome 浏览器无差异,且不受本机安装的浏览器版本影响。 **验收标准**: - [ ] 客户端内嵌现代 Chromium 内核(如基于 Electron 或 WebView2),版本不低于 Chromium 100,支持现代 Web 标准(ES2020、CSS Grid、Fetch API 等) @@ -95,7 +95,7 @@ Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通 ### Story 3:客户端感知新版本并自动升级 -**As** 一线经纪人,**I want** 客户端在有新版本时自动提示并完成升级,**So that** 我无需手动下载安装,始终使用最新版本,不会因版本落后导致功能异常。 +**As** Agent(经纪人),**I want** 客户端在有新版本时自动提示并完成升级,**So that** 我无需手动下载安装,始终使用最新版本,不会因版本落后导致功能异常。 **验收标准**: - [ ] 客户端启动时及运行期间(每隔 4 小时)自动向更新服务器检查最新版本 @@ -108,9 +108,9 @@ Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通 --- -### Story 4:系统管理员发布新版本 +### Story 4:Tenant Admin(租户管理员)发布新版本 -**As** 系统管理员,**I want** 通过管理后台上传新版客户端安装包并配置版本信息,**So that** 客户端能感知到更新并引导用户升级。 +**As** Tenant Admin(租户管理员),**I want** 通过管理后台上传新版客户端安装包并配置版本信息,**So that** 客户端能感知到更新并引导用户升级。 **验收标准**: - [ ] 系统管理后台提供"客户端版本管理"页面(位于系统管理模块下) @@ -125,7 +125,7 @@ Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通 ### Story 5:管理员监控客户端版本分布 -**As** 系统管理员,**I want** 查看当前所有在线客户端的版本分布情况,**So that** 了解升级覆盖率,对仍在使用旧版本的客户端发出提醒或强制升级。 +**As** Tenant Admin(租户管理员),**I want** 查看当前所有在线客户端的版本分布情况,**So that** 了解升级覆盖率,对仍在使用旧版本的客户端发出提醒或强制升级。 **验收标准**: - [ ] 客户端版本管理页面展示版本分布统计:各版本在线客户端数量及占比(饼图或条形图) diff --git a/Project/fonrey/PRD/客源管理/客源管理模块PRD.md b/Project/fonrey/PRD/客源管理/客源管理模块PRD.md index fe7523c2..07d80fdf 100644 --- a/Project/fonrey/PRD/客源管理/客源管理模块PRD.md +++ b/Project/fonrey/PRD/客源管理/客源管理模块PRD.md @@ -26,7 +26,7 @@ | 角色 | 描述 | 使用频率 | |------|------|----------| -| 一线经纪人 | 负责录入、维护、跟进自己名下的客源,并与客户进行带看匹配 | 每日高频 | +| Agent(经纪人) | 负责录入、维护、跟进自己名下的客源,并与客户进行带看匹配 | 每日高频 | | 店长/经理 | 查看全店/全区客源概况,监控跟进完成度及带看活跃度 | 每日 | | 行政人员 | 审核客源信息,处理重复客户合并、来源修改审批等 | 每日 | @@ -62,7 +62,7 @@ ### Story 1:经纪人录入新私客 -**As** 一线经纪人,**I want** 快速录入一位新客户的基本信息和购房/租房需求,**So that** 客源进入系统统一管理并支持后续的智能配房和跟进管理。 +**As** Agent(经纪人),**I want** 快速录入一位新客户的基本信息和购房/租房需求,**So that** 客源进入系统统一管理并支持后续的智能配房和跟进管理。 **验收标准**: - [ ] 录入页面标题为"录入私客",通过顶部导航「客源」→「+ 新增私客」或右侧浮动快捷入口「增客」触达 @@ -81,7 +81,7 @@ ### Story 2:经纪人查看与筛选私客列表 -**As** 一线经纪人,**I want** 在客源列表中快速定位目标客户并了解其跟进状态,**So that** 提升客户管理效率,优先跟进高意向客户。 +**As** Agent(经纪人),**I want** 在客源列表中快速定位目标客户并了解其跟进状态,**So that** 提升客户管理效率,优先跟进高意向客户。 **验收标准**: - [ ] 顶部 Tab 导航按需求类型分组:**求购 / 求租 / 暂缓 / 全部私客**,Tab 间可自由切换 @@ -100,7 +100,7 @@ ### Story 3:经纪人查看求购私客列表 -**As** 一线经纪人,**I want** 在求购 Tab 下查看所有有购房意向的私客,并通过多维度筛选快速定位目标客户,**So that** 优先匹配合适房源,提升成交效率。 +**As** Agent(经纪人),**I want** 在求购 Tab 下查看所有有购房意向的私客,并通过多维度筛选快速定位目标客户,**So that** 优先匹配合适房源,提升成交效率。 **验收标准**: - [ ] 求购 Tab 下,状态筛选项为:不限 / 求购 / 租购 @@ -116,7 +116,7 @@ ### Story 4:经纪人查看求租私客列表 -**As** 一线经纪人,**I want** 在求租 Tab 下查看所有有租房意向的私客,**So that** 快速为其匹配合适的出租房源。 +**As** Agent(经纪人),**I want** 在求租 Tab 下查看所有有租房意向的私客,**So that** 快速为其匹配合适的出租房源。 **验收标准**: - [ ] 求租 Tab 下,状态筛选项为:不限 / 求租 / 租购 @@ -129,7 +129,7 @@ ### Story 5:经纪人管理暂缓私客 -**As** 一线经纪人,**I want** 将暂时没有购房/租房需求的客户标记为"暂缓"状态,**So that** 保持活跃客源列表的精准性,同时不丢失潜在客户资源。 +**As** Agent(经纪人),**I want** 将暂时没有购房/租房需求的客户标记为"暂缓"状态,**So that** 保持活跃客源列表的精准性,同时不丢失潜在客户资源。 **验收标准**: - [ ] 暂缓 Tab 下仅展示状态为"暂缓"的客源 @@ -140,7 +140,7 @@ ### Story 6:经纪人查看私客详情页 -**As** 一线经纪人,**I want** 点击客源姓名后进入详情页,查看该客户的完整信息和快捷操作入口,**So that** 在一个页面内完成客户的跟进、带看、配房等核心操作。 +**As** Agent(经纪人),**I want** 点击客源姓名后进入详情页,查看该客户的完整信息和快捷操作入口,**So that** 在一个页面内完成客户的跟进、带看、配房等核心操作。 **验收标准**: - [ ] 详情页顶部展示客源标题(需求标题+联系人姓名),旁边显示带看进度标签(如"一看") @@ -158,7 +158,7 @@ ### Story 7:经纪人查看与编辑需求信息 -**As** 一线经纪人,**I want** 在需求信息 Tab 中查看客户的完整购房/租房需求字段,并支持编辑,**So that** 确保需求信息准确以提升智能配房的匹配精度。 +**As** Agent(经纪人),**I want** 在需求信息 Tab 中查看客户的完整购房/租房需求字段,并支持编辑,**So that** 确保需求信息准确以提升智能配房的匹配精度。 **验收标准**: - [ ] 需求信息 Tab 下展示以下字段(三栏布局):总价(范围区间,万元)、面积(范围区间,㎡)、居室(X居格式)、装修(下拉枚举)、朝向(多选枚举)、楼层(多选:中楼层/低楼层等)、楼龄(范围区间)、意向商圈(多选)、意向小区(多选)、交通情况(文本)、备注(文本) @@ -170,7 +170,7 @@ ### Story 8:经纪人写入与查看跟进记录 -**As** 一线经纪人,**I want** 在跟进记录 Tab 中记录每一次与客户的沟通内容,并按类型筛选查看,**So that** 保留完整的跟进轨迹,便于持续维护客户关系。 +**As** Agent(经纪人),**I want** 在跟进记录 Tab 中记录每一次与客户的沟通内容,并按类型筛选查看,**So that** 保留完整的跟进轨迹,便于持续维护客户关系。 **验收标准**: - [ ] 跟进记录区块顶部为 5 个子 Tab:全部 / 写入跟进 / 敏感信息跟进 / 修改跟进 / 其他跟进 @@ -189,7 +189,7 @@ ### Story 9:经纪人管理带看记录 -**As** 一线经纪人,**I want** 在带看 Tab 中记录与该客户的预约带看和实际带看情况,**So that** 系统化追踪带看进度,提升成交转化效率。 +**As** Agent(经纪人),**I want** 在带看 Tab 中记录与该客户的预约带看和实际带看情况,**So that** 系统化追踪带看进度,提升成交转化效率。 **验收标准**: - [ ] 带看区块顶部提供两个子 Tab:**预约** / **带看** @@ -217,7 +217,7 @@ ### Story 10:经纪人查看客源解读(AI行为分析) -**As** 一线经纪人,**I want** 在客源解读 Tab 中查看系统根据该客户的找房行为自动生成的偏好分析,**So that** 更准确地判断客户真实需求和购房意向,提升推房精准度。 +**As** Agent(经纪人),**I want** 在客源解读 Tab 中查看系统根据该客户的找房行为自动生成的偏好分析,**So that** 更准确地判断客户真实需求和购房意向,提升推房精准度。 **验收标准**: - [ ] 客源解读 Tab 展示说明文字:"以下数据通过客户找房行为分析生成,百分比越高代表浏览次数多",右上角提供「使用指南」链接 @@ -235,7 +235,7 @@ ### Story 11:经纪人使用二手配房功能推荐房源 -**As** 一线经纪人,**I want** 在智能配房(二手配房)Tab 中查看系统为该客户匹配的房源列表,并将合适房源分享给客户,**So that** 提升推房效率,加快成交进度。 +**As** Agent(经纪人),**I want** 在智能配房(二手配房)Tab 中查看系统为该客户匹配的房源列表,并将合适房源分享给客户,**So that** 提升推房效率,加快成交进度。 **验收标准**: - [ ] 二手配房区块标题右侧显示最后更新时间(如"2026-04-22 17:38 更新")及刷新说明 ⓘ 图标 @@ -262,7 +262,7 @@ ### Story 12:经纪人查看与筛选公客列表 -**As** 一线经纪人,**I want** 在公客 Tab 中查看全公司公共客源池中的客户,并通过多维度筛选快速找到我有能力跟进的公客,**So that** 利用公客资源开拓新成交机会。 +**As** Agent(经纪人),**I want** 在公客 Tab 中查看全公司公共客源池中的客户,并通过多维度筛选快速找到我有能力跟进的公客,**So that** 利用公客资源开拓新成交机会。 **验收标准**: - [ ] 顶部一级 Tab 切换至「公客」后激活该视图,Tab 标题高亮橙色下划线 @@ -312,7 +312,7 @@ ### Story 13:经纪人查看成交客列表 -**As** 一线经纪人,**I want** 在成交客 Tab 中查看已完成成交的客户列表,并通过筛选快速回顾历史成交记录,**So that** 为复购/转介绍跟进和业绩复盘提供数据支撑。 +**As** Agent(经纪人),**I want** 在成交客 Tab 中查看已完成成交的客户列表,并通过筛选快速回顾历史成交记录,**So that** 为复购/转介绍跟进和业绩复盘提供数据支撑。 **验收标准**: - [ ] 顶部一级 Tab 切换至「成交客」后激活该视图(面包屑:客源 / 客源管理 / 成交客) @@ -353,7 +353,7 @@ ### Story 14:经纪人编辑客源信息 -**As** 一线经纪人,**I want** 对已录入的客源进行编辑,修改联系方式、基础信息或购房需求,**So that** 保持客源数据的准确性,提升智能配房匹配精度。 +**As** Agent(经纪人),**I want** 对已录入的客源进行编辑,修改联系方式、基础信息或购房需求,**So that** 保持客源数据的准确性,提升智能配房匹配精度。 **验收标准**: - [ ] 编辑客源页面标题为「编辑客源」,通过私客详情页右侧「编辑」按钮或需求信息 Tab「编辑」链接触达 @@ -404,7 +404,7 @@ ### Story 15:经纪人查看客源信息概览面板 -**As** 一线经纪人,**I want** 在客源详情页右侧快速概览该客源的核心信息和常用操作,**So that** 无需切换 Tab 即可掌握客户关键状态并快速执行高频操作。 +**As** Agent(经纪人),**I want** 在客源详情页右侧快速概览该客源的核心信息和常用操作,**So that** 无需切换 Tab 即可掌握客户关键状态并快速执行高频操作。 **验收标准**: - [ ] 客源详情页右侧固定展示"信息概览"面板,不随页面滚动消失 @@ -422,7 +422,7 @@ ### Story 16:经纪人收藏客源至私客收藏夹 -**As** 一线经纪人,**I want** 将重点客户收藏至私客收藏夹,**So that** 在私客列表中可按收藏夹快速筛选出重点跟进客户。 +**As** Agent(经纪人),**I want** 将重点客户收藏至私客收藏夹,**So that** 在私客列表中可按收藏夹快速筛选出重点跟进客户。 **验收标准**: - [ ] 点击信息概览面板中的「☆ 收藏」触发"选择私客收藏夹"浮层 @@ -440,7 +440,7 @@ ### Story 17:经纪人修改客源等级 -**As** 一线经纪人,**I want** 快速修改客源的购买意向等级,**So that** 确保客源等级与客户当前实际意向相符,辅助工作优先级排序。 +**As** Agent(经纪人),**I want** 快速修改客源的购买意向等级,**So that** 确保客源等级与客户当前实际意向相符,辅助工作优先级排序。 **验收标准**: - [ ] 点击信息概览面板中的「改等级」触发"改等级"弹窗 @@ -456,7 +456,7 @@ ### Story 18:经纪人修改客源状态 -**As** 一线经纪人,**I want** 修改客源的需求状态(求购/求租/租购),**So that** 准确反映客户当前的业务需求方向,保证配房匹配精准。 +**As** Agent(经纪人),**I want** 修改客源的需求状态(求购/求租/租购),**So that** 准确反映客户当前的业务需求方向,保证配房匹配精准。 **验收标准**: - [ ] 点击信息概览面板中的「改状态」触发"改状态"弹窗 @@ -473,7 +473,7 @@ ### Story 19:经纪人手动将私客转为公客 -**As** 一线经纪人或店长,**I want** 主动将某私客转入公共客源池,**So that** 让其他经纪人也能跟进该客户,提升客源利用率和成交机会。 +**As** Agent(经纪人)或店长,**I want** 主动将某私客转入公共客源池,**So that** 让其他经纪人也能跟进该客户,提升客源利用率和成交机会。 **验收标准**: - [ ] 点击信息概览面板中的「转公客」触发"把此私客转为公客"弹窗 @@ -492,7 +492,7 @@ ### Story 20:经纪人将私客转为成交客 -**As** 一线经纪人,**I want** 在客户完成购房/租房成交后将其标记为成交客,并录入成交信息,**So that** 完整记录成交数据,同步到成交客列表及业绩统计。 +**As** Agent(经纪人),**I want** 在客户完成购房/租房成交后将其标记为成交客,并录入成交信息,**So that** 完整记录成交数据,同步到成交客列表及业绩统计。 **验收标准**: - [ ] 点击信息概览面板中的「转成交」触发"客户成交"弹窗 @@ -526,7 +526,7 @@ ### Story 21:经纪人将客源标记为无效 -**As** 一线经纪人,**I want** 将无效客户(如空号/骚扰/无意向等)标记为无效,**So that** 避免对无效客户持续无效跟进,提升客源列表质量。 +**As** Agent(经纪人),**I want** 将无效客户(如空号/骚扰/无意向等)标记为无效,**So that** 避免对无效客户持续无效跟进,提升客源列表质量。 **验收标准**: - [ ] 点击信息概览面板中的「转无效」触发"请选择无效原因"弹窗 @@ -549,7 +549,7 @@ ### Story 22:经纪人编辑客源基础信息(快捷入口) -**As** 一线经纪人,**I want** 通过信息概览面板的快捷入口直接编辑客源基础信息,**So that** 无需进入完整编辑页即可快速更新常用字段。 +**As** Agent(经纪人),**I want** 通过信息概览面板的快捷入口直接编辑客源基础信息,**So that** 无需进入完整编辑页即可快速更新常用字段。 **验收标准**: - [ ] 点击信息概览面板中的「编辑客源」入口,触发"编辑基础信息"弹窗(抽屉浮层) @@ -577,7 +577,7 @@ ### Story 23:经纪人管理客源联系人 -**As** 一线经纪人,**I want** 在客源详情页查看、新增或编辑该客源的联系人信息,**So that** 确保联系方式信息完整,随时可以联系到客户。 +**As** Agent(经纪人),**I want** 在客源详情页查看、新增或编辑该客源的联系人信息,**So that** 确保联系方式信息完整,随时可以联系到客户。 **验收标准**: @@ -620,7 +620,7 @@ ### Story 24:经纪人管理客源相关员工 -**As** 一线经纪人或店长,**I want** 查看和修改客源的首录人与归属人,**So that** 确保客源归属关系正确,保证客源跟进责任到人。 +**As** Agent(经纪人)或店长,**I want** 查看和修改客源的首录人与归属人,**So that** 确保客源归属关系正确,保证客源跟进责任到人。 **验收标准**: @@ -649,7 +649,7 @@ ### Story 25:经纪人查看客源操作日志 -**As** 一线经纪人或店长,**I want** 查看某一客源的完整操作历史记录,**So that** 了解客源的全生命周期操作轨迹,追溯变更原因,辅助审计和管理。 +**As** Agent(经纪人)或店长,**I want** 查看某一客源的完整操作历史记录,**So that** 了解客源的全生命周期操作轨迹,追溯变更原因,辅助审计和管理。 **验收标准**: diff --git a/Project/fonrey/PRD/房源管理/房源管理模块PRD.md b/Project/fonrey/PRD/房源管理/房源管理模块PRD.md index c7eedc29..0be3108e 100644 --- a/Project/fonrey/PRD/房源管理/房源管理模块PRD.md +++ b/Project/fonrey/PRD/房源管理/房源管理模块PRD.md @@ -26,7 +26,7 @@ | 角色 | 描述 | 使用频率 | |------|------|----------| -| 一线经纪人 | 负责录入、维护、跟进自己名下的房源 | 每日高频 | +| Agent(经纪人) | 负责录入、维护、跟进自己名下的房源 | 每日高频 | | 店长/经理 | 查看全店/全区房源概况,分配任务,监控跟进完成度 | 每日 | | 行政人员 | 审核房源信息,维护数据质量 | 每日 | @@ -61,7 +61,7 @@ ### Story 1:经纪人录入新房源 -**As** 一线经纪人,**I want** 快速录入一套新房源的完整信息,**So that** 房源能进入公盘流通并留存完整的基础数据。 +**As** Agent(经纪人),**I want** 快速录入一套新房源的完整信息,**So that** 房源能进入公盘流通并留存完整的基础数据。 **验收标准**: - [ ] 支持选择 6 种房源类型(住宅、别墅、商铺、商住、写字楼、其他),本期 P0 实现住宅,P1 实现别墅,商业类型低优先级 @@ -79,7 +79,7 @@ ### Story 2:经纪人筛选查找目标房源 -**As** 一线经纪人,**I want** 在数万条房源中快速定位符合客户需求的房源,**So that** 提升匹配推荐效率、减少无效沟通。 +**As** Agent(经纪人),**I want** 在数万条房源中快速定位符合客户需求的房源,**So that** 提升匹配推荐效率、减少无效沟通。 **验收标准**: - [ ] 支持关键词搜索:小区名称、地址、业主姓名、电话、钥匙编号等 @@ -100,7 +100,7 @@ ### Story 3:经纪人变更房源状态 -**As** 一线经纪人,**I want** 在房源详情页快速变更房源的交易状态并说明原因,**So that** 保持房源状态与实际情况同步,团队成员能及时感知。 +**As** Agent(经纪人),**I want** 在房源详情页快速变更房源的交易状态并说明原因,**So that** 保持房源状态与实际情况同步,团队成员能及时感知。 **验收标准**: - [ ] 状态变更入口位于详情页顶部操作区"改状态"按钮 @@ -113,7 +113,7 @@ ### Story 4:经纪人在详情页快捷编辑房源核心字段 -**As** 一线经纪人,**I want** 不离开房源详情页就能直接修改价格、等级、属性、现状、用途等高频字段,**So that** 减少页面跳转,快速响应业主意向变化。 +**As** Agent(经纪人),**I want** 不离开房源详情页就能直接修改价格、等级、属性、现状、用途等高频字段,**So that** 减少页面跳转,快速响应业主意向变化。 **验收标准**: - [ ] 详情页顶部支持快捷编辑栋座单元房号(含更改理由,最多 200 字) @@ -131,7 +131,7 @@ ### Story 5:经纪人记录房源跟进 -**As** 一线经纪人,**I want** 随时记录与业主的沟通内容,**So that** 保留完整的跟进轨迹,团队协作时不会重复打扰业主。 +**As** Agent(经纪人),**I want** 随时记录与业主的沟通内容,**So that** 保留完整的跟进轨迹,团队协作时不会重复打扰业主。 **验收标准**: - [ ] 在房源详情页顶部可直接唤起"写跟进"浮层,无需离开当前页面 @@ -151,7 +151,7 @@ ### Story 6:经纪人管理钥匙与委托 -**As** 一线经纪人,**I want** 在系统中登记钥匙持有情况和委托协议信息,**So that** 提升房源维护完成度评分,并让团队知悉钥匙状态,避免带看时出现无法入场的情况。 +**As** Agent(经纪人),**I want** 在系统中登记钥匙持有情况和委托协议信息,**So that** 提升房源维护完成度评分,并让团队知悉钥匙状态,避免带看时出现无法入场的情况。 **验收标准**: @@ -172,7 +172,7 @@ ### Story 7:经纪人提交实勘记录 -**As** 一线经纪人,**I want** 完成实地勘察后在系统中提交实勘报告和分类照片,**So that** 提升房源维护完成度、保障房源信息真实性,给客户提供更可信的参考依据。 +**As** Agent(经纪人),**I want** 完成实地勘察后在系统中提交实勘报告和分类照片,**So that** 提升房源维护完成度、保障房源信息真实性,给客户提供更可信的参考依据。 **验收标准**: - [ ] 实勘为独立页面,包含:实勘定位(GPS)、实勘说明(最多 200 字) @@ -186,7 +186,7 @@ ### Story 8:经纪人管理房源业主与联系人 -**As** 一线经纪人,**I want** 在房源详情页随时新增、编辑业主/联系人信息,并查看该业主名下的其他房源,**So that** 保持联系人资料准确完整,了解业主资产全貌,避免重复打扰、提升沟通效率。 +**As** Agent(经纪人),**I want** 在房源详情页随时新增、编辑业主/联系人信息,并查看该业主名下的其他房源,**So that** 保持联系人资料准确完整,了解业主资产全貌,避免重复打扰、提升沟通效率。 #### 验收标准 8.1:新增业主/联系人 @@ -222,7 +222,7 @@ ### Story 9:经纪人管理房源图片 -**As** 一线经纪人,**I want** 为房源上传并分类管理照片、设置封面图,**So that** 提升房源展示质量,吸引更多买家/租客关注。 +**As** Agent(经纪人),**I want** 为房源上传并分类管理照片、设置封面图,**So that** 提升房源展示质量,吸引更多买家/租客关注。 **验收标准**: - [ ] 支持图片分类:封面(限 1 张)、玄关、客厅、餐厅、卧室、卫生间、厨房、阳台、书房、室内其他、室外、全景 @@ -235,7 +235,7 @@ ### Story 10:经纪人管理房源附件 -**As** 一线经纪人,**I want** 为房源上传并分类管理证件/协议等附件文件,**So that** 将核心证明材料与房源绑定留存,方便后续合规审查和团队共享查阅。 +**As** Agent(经纪人),**I want** 为房源上传并分类管理证件/协议等附件文件,**So that** 将核心证明材料与房源绑定留存,方便后续合规审查和团队共享查阅。 **验收标准**: - [ ] 附件分类 Tab 包含:全部 / 身份证 / 房产证 / 委托书 / 其他,每个 Tab 实时显示该分类文件数量 @@ -248,7 +248,7 @@ ### Story 11:经纪人补全房源详细信息 -**As** 一线经纪人,**I want** 不离开房源详情页补充或修改基本信息、产证信息、房屋介绍和楼盘信息,**So that** 提升房源维护完成度评分,增强房源对买家/租客的吸引力。 +**As** Agent(经纪人),**I want** 不离开房源详情页补充或修改基本信息、产证信息、房屋介绍和楼盘信息,**So that** 提升房源维护完成度评分,增强房源对买家/租客的吸引力。 **验收标准**: - [ ] 「房源信息」Tab 下,以下区块均支持点击"编辑"触发浮窗编辑,无需跳转页面: diff --git a/Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md b/Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md index 7aa3bea5..d8981bc2 100644 --- a/Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md +++ b/Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md @@ -27,9 +27,9 @@ | 角色 | 描述 | 使用频率 | |------|------|----------| | 运营/数据管理员 | 维护楼盘信息、楼栋结构、区域体系、学校信息的标准化数据 | 每日 | -| 一线经纪人 | 查询楼盘详情、参考价格走势、了解周边配套辅助成交 | 每日 | +| Agent(经纪人) | 查询楼盘详情、参考价格走势、了解周边配套辅助成交 | 每日 | | 店长/经理 | 监控楼盘数据完整度,分析区域市场行情 | 每周 | -| 系统管理员 | 配置区域关联关系,管理数据标准 | 不定期 | +| Tenant Admin(租户管理员) | 配置区域关联关系,管理数据标准 | 不定期 | --- @@ -227,7 +227,7 @@ ### Story 7:经纪人查看楼盘价格走势 -**As** 一线经纪人,**I want** 在楼盘详情页查看该楼盘的挂牌价走势和历史成交数据,**So that** 在带看时能为客户提供客观的市场行情参考,增强议价信心。 +**As** Agent(经纪人),**I want** 在楼盘详情页查看该楼盘的挂牌价走势和历史成交数据,**So that** 在带看时能为客户提供客观的市场行情参考,增强议价信心。 **验收标准**: @@ -263,7 +263,7 @@ ### Story 8:经纪人查看楼盘周边配套 -**As** 一线经纪人,**I want** 在楼盘详情页查看该楼盘周边的交通/教育/医疗/购物/生活/娱乐配套,**So that** 在带客时快速回答客户关于生活便利性的问题,增强成交转化。 +**As** Agent(经纪人),**I want** 在楼盘详情页查看该楼盘周边的交通/教育/医疗/购物/生活/娱乐配套,**So that** 在带客时快速回答客户关于生活便利性的问题,增强成交转化。 **验收标准**: - [ ] 周边配套 Tab 以地图为主体,楼盘位置以橙色标记点展示在地图上 diff --git a/Project/fonrey/PRD/权限管理/小区.md b/Project/fonrey/PRD/权限管理/小区.md index 2d679dbc..80b0c987 100644 --- a/Project/fonrey/PRD/权限管理/小区.md +++ b/Project/fonrey/PRD/权限管理/小区.md @@ -20,7 +20,7 @@ | 司内成交明细及套数 | true/false | 开启后,显示公司成交的房源明细信息及成交套数 | | 区域管理 | true/false | 若启用,则可对区域商圈进行新增、合并、关联操作 | | 查看销控盘 | true/false | 开启后,可在楼盘管理系统-楼盘里,查看销控盘。请注意:员工查看销控盘时房源地址是直接可见的,建议只给管理层开启!!! | -| 查看销控盘时,只可查看本部门作业范围内的楼盘 | true/false | 开启后,只可查看本部门作业范围内的楼盘的销控盘;关闭后,则跟作业范围无关,「查看销控盘」权限开启即可见所有楼盘的销控盘;系统管理员不受限制 | +| 查看销控盘时,只可查看本部门作业范围内的楼盘 | true/false | 开启后,只可查看本部门作业范围内的楼盘的销控盘;关闭后,则跟作业范围无关,「查看销控盘」权限开启即可见所有楼盘的销控盘;Tenant Admin(租户管理员)不受限制 | --- diff --git a/Project/fonrey/PRD/权限管理/权限管理模块PRD.md b/Project/fonrey/PRD/权限管理/权限管理模块PRD.md index 8dadd164..8ee24824 100644 --- a/Project/fonrey/PRD/权限管理/权限管理模块PRD.md +++ b/Project/fonrey/PRD/权限管理/权限管理模块PRD.md @@ -24,9 +24,9 @@ | 角色 | 描述 | 使用频率 | |------|------|----------| -| 系统管理员 | 负责角色创建、权限配置、人员权限分配及批量操作,是本模块的核心使用者 | 每日 | +| Tenant Admin(租户管理员) | 负责角色创建、权限配置、人员权限分配及批量操作,是本模块的核心使用者 | 每日 | | 店长 / 区域经理 | 查看下属员工当前权限,按需发起权限变更申请 | 按需 | -| 一线经纪人 | 受权限约束的数据访问者,感知到功能入口的显示/隐藏变化 | 被动感知 | +| Agent(经纪人) | 受权限约束的数据访问者,感知到功能入口的显示/隐藏变化 | 被动感知 | --- @@ -60,7 +60,7 @@ ### Story 1:管理员查看人员权限列表 -**As** 系统管理员,**I want** 在人员列表中查看全公司所有员工的当前角色与权限状态,**So that** 能快速定位权限异常人员并执行调整。 +**As** Tenant Admin(租户管理员),**I want** 在人员列表中查看全公司所有员工的当前角色与权限状态,**So that** 能快速定位权限异常人员并执行调整。 **验收标准**: - [ ] 页面入口路径:顶部导航「人事」→「组织人事」→「权限管理」,面包屑显示「人事OA / 组织人事 / 权限管理」 @@ -81,7 +81,7 @@ ### Story 2:管理员为员工批量设置角色 -**As** 系统管理员,**I want** 同时为多名员工批量设置同一角色,**So that** 在员工入职或组织架构调整时能高效完成权限初始化,无需逐一操作。 +**As** Tenant Admin(租户管理员),**I want** 同时为多名员工批量设置同一角色,**So that** 在员工入职或组织架构调整时能高效完成权限初始化,无需逐一操作。 **验收标准**: - [ ] 在人员列表中勾选至少一名员工后,「批量设置角色」按钮从禁用变为可点击 @@ -95,7 +95,7 @@ ### Story 3:管理员修改个人权限 -**As** 系统管理员,**I want** 为特定员工在角色权限基础上进行个性化权限调整,**So that** 满足因岗位特殊性而需要区别于通用角色权限配置的场景。 +**As** Tenant Admin(租户管理员),**I want** 为特定员工在角色权限基础上进行个性化权限调整,**So that** 满足因岗位特殊性而需要区别于通用角色权限配置的场景。 **验收标准**: - [ ] 点击人员列表中某员工操作列的「修改权限」后,进入该员工的权限编辑页 @@ -132,7 +132,7 @@ ### Story 4:管理员查看并编辑特定权限项(侧边抽屉) -**As** 系统管理员,**I want** 在编辑单个权限项时,同时看到该权限当前应用该角色的所有人员名单,**So that** 能了解本次修改的影响范围,再决定是否确认变更。 +**As** Tenant Admin(租户管理员),**I want** 在编辑单个权限项时,同时看到该权限当前应用该角色的所有人员名单,**So that** 能了解本次修改的影响范围,再决定是否确认变更。 **验收标准**: - [ ] 点击权限项的「编辑」按钮后,页面右侧滑出 Drawer(不覆盖左侧导航) @@ -147,7 +147,7 @@ ### Story 5:管理员查看角色列表 -**As** 系统管理员,**I want** 在角色管理页查看所有已创建的角色及其基本信息,**So that** 快速了解当前系统的角色体系,并按需编辑或删除。 +**As** Tenant Admin(租户管理员),**I want** 在角色管理页查看所有已创建的角色及其基本信息,**So that** 快速了解当前系统的角色体系,并按需编辑或删除。 **验收标准**: - [ ] 点击顶部「角色管理」Tab 切换至角色列表页 @@ -165,7 +165,7 @@ ### Story 6:管理员新增角色 -**As** 系统管理员,**I want** 新建一个角色并配置其权限,**So that** 为新增岗位或特殊职能定义标准权限模板,便于批量分配给对应员工。 +**As** Tenant Admin(租户管理员),**I want** 新建一个角色并配置其权限,**So that** 为新增岗位或特殊职能定义标准权限模板,便于批量分配给对应员工。 **验收标准**: - [ ] 点击「+ 新增角色」按钮弹出 Modal,标题为「添加角色」 @@ -182,7 +182,7 @@ ### Story 7:管理员配置角色权限 -**As** 系统管理员,**I want** 在角色权限编辑页为角色精细配置各模块的权限开关和数据范围,**So that** 该角色下所有人员自动继承统一的权限配置,减少重复操作。 +**As** Tenant Admin(租户管理员),**I want** 在角色权限编辑页为角色精细配置各模块的权限开关和数据范围,**So that** 该角色下所有人员自动继承统一的权限配置,减少重复操作。 **验收标准**: - [ ] 角色权限编辑页顶部展示:角色名称、角色类别、「编辑」按钮(支持在线修改角色基本信息) @@ -199,7 +199,7 @@ ### Story 8:管理员修改角色(切换员工角色) -**As** 系统管理员,**I want** 在员工权限编辑页直接切换员工所属角色,**So that** 快速完成角色变更而不需要退出到人员列表再操作。 +**As** Tenant Admin(租户管理员),**I want** 在员工权限编辑页直接切换员工所属角色,**So that** 快速完成角色变更而不需要退出到人员列表再操作。 **验收标准**: - [ ] 在员工权限编辑页顶部,角色名称旁有下拉箭头,点击展开角色选择器 @@ -427,7 +427,7 @@ Fonrey 采用 **RBAC(基于角色的访问控制)+ 个人权限叠加** 的 | 司内成交明细及套数 | 开关型 | 开启后,显示公司成交的房源明细信息及成交套数 | | 区域管理 | 开关型 | 若启用,则可对区域商圈进行新增、合并、关联操作 | | 查看销控盘 | 开关型 | 开启后,可在楼盘管理系统-楼盘里,查看销控盘;注意:员工查看销控盘时房源地址是直接可见的,建议只给管理层开启!!!| -| 查看销控盘时,只可查看本部门作业范围内的楼盘 | 开关型 | 开启后,只可查看本部门作业范围内的楼盘的销控盘;关闭,则跟作业范围无关,「查看销控盘」权限开启即可查看所有楼盘的销控盘;系统管理员不受限制 | +| 查看销控盘时,只可查看本部门作业范围内的楼盘 | 开关型 | 开启后,只可查看本部门作业范围内的楼盘的销控盘;关闭,则跟作业范围无关,「查看销控盘」权限开启即可查看所有楼盘的销控盘;Tenant Admin(租户管理员)不受限制 | **楼盘资料管理** @@ -537,8 +537,8 @@ Fonrey 采用 **RBAC(基于角色的访问控制)+ 个人权限叠加** 的 | 阶段 | 时间 | 受众 | 通过标准 | |------|------|------|---------| | 内部 Alpha | TBD | 研发 + 产品团队 | 角色 CRUD 流程通畅,权限编辑保存无误 | -| 封闭 Beta | TBD | 1-2 家试点门店系统管理员 | 批量设置角色 / 个人权限修改功能可用,无 P0 Bug | -| 正式上线 | TBD | 全部租户系统管理员 | 权限变更实时生效,错误率 < 0.5%,系统管理员 CSAT ≥ 4/5 | +| 封闭 Beta | TBD | 1-2 家试点门店Tenant Admin(租户管理员) | 批量设置角色 / 个人权限修改功能可用,无 P0 Bug | +| 正式上线 | TBD | 全部租户Tenant Admin(租户管理员) | 权限变更实时生效,错误率 < 0.5%,Tenant Admin(租户管理员) CSAT ≥ 4/5 | **回滚标准**:若权限写入后错误率 > 2%,或出现跨租户数据泄漏问题,立即回滚并通知所有租户管理员。 diff --git a/Project/fonrey/PRD/登录管理/找回密码流程.png b/Project/fonrey/PRD/登录管理/找回密码流程.png deleted file mode 100644 index 63df3f24..00000000 Binary files a/Project/fonrey/PRD/登录管理/找回密码流程.png and /dev/null differ diff --git a/Project/fonrey/PRD/登录管理/找回用户名流程.png b/Project/fonrey/PRD/登录管理/找回用户名流程.png deleted file mode 100644 index 7fba481f..00000000 Binary files a/Project/fonrey/PRD/登录管理/找回用户名流程.png and /dev/null differ diff --git a/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md b/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md index eda83556..530d3148 100644 --- a/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md +++ b/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md @@ -2,8 +2,8 @@ **状态**: Draft **作者**: 产品经理 -**最后更新**: 2026-04-25(v1.4 §5.5 后端数据模型迁移至独立文档 `DATA_MODEL/DATA_MODEL_LOGIN.md`) -**版本**: 1.4 +**最后更新**: 2026-04-30(v2.0 根据 review 后的 §4 用户故事全面同步 §5 功能详细说明:删除找回用户名流程及邮件模板;找回密码改为纯短信流程;新增 §5.5 手机验证码登录详细说明;§6 技术注意事项更新短信依赖/风险/开放问题;§8.2 接口清单同步正式功能状态) +**版本**: 2.0 **所属系统**: Fonrey 房产经纪管理系统 **关联模块**: 组织人事管理、权限管理、系统管理 @@ -28,17 +28,17 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架 |------|--------|---------| | 多租户环境下,客户端不知道应该连接哪个租户的服务端 | 新用户首次安装后无法正常使用 | 系统无法启动,用户体验极差 | | 账号密码裸露登录,缺乏验证码保护 | 所有用户 | 存在暴力破解、自动化恶意登录风险 | -| 用户忘记账号或密码无自助找回通道 | 一线经纪人 | 依赖管理员手动重置,效率低 | -| 账号未与实名经纪人档案绑定 | 系统管理员、合规审计 | 操作行为无法追溯至自然人 | +| 用户忘记账号或密码无自助找回通道 | Agent(经纪人) | 依赖管理员手动重置,效率低 | +| 账号未与实名经纪人档案绑定 | Tenant Admin(租户管理员)、合规审计 | 操作行为无法追溯至自然人 | | 无多因素认证,安全系数低 | 管理层、数据合规 | 存在账号冒用、数据泄露风险 | ### 1.3 目标用户 | 角色 | 描述 | 使用频率 | |------|------|----------| -| 一线经纪人 | 每日登录系统使用房源/客源功能 | 每日高频 | +| Agent(经纪人) | 每日登录系统使用房源/客源功能 | 每日高频 | | 店长 / 经理 | 登录后查看全店数据、管理任务 | 每日 | -| 系统管理员 | 管理账号创建、密码重置、租户初始化 | 按需 | +| Tenant Admin(租户管理员) | 管理账号创建、密码重置、租户初始化 | 按需 | | 新安装用户 | 首次安装客户端后需完成 Tenant 识别 | 一次性 | --- @@ -49,7 +49,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架 |------|------|----------|--------|----------| | 降低登录失败率 | 账号密码正确情况下的登录成功率 | 待统计 | ≥ 99% | 上线后 30 天 | | 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0(无保护) | 建立基线,同IP异常次数 > 5次/分钟触发封锁 | 上线后持续监控 | -| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约1工作日) | < 3 分钟(邮件/短信自助) | 上线后 30 天 | +| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约1工作日) | < 3 分钟(短信验证码自助) | 上线后 30 天 | | 确保账号实名绑定率 | 拥有系统账号且未与员工档案绑定的账号比例 | 待统计 | 0%(强制绑定) | 上线即达标 | | Tenant 识别成功率 | 首次安装后成功完成 Tenant 识别的用户比例 | 待统计 | ≥ 98% | 上线后 30 天 | @@ -58,11 +58,12 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架 ## 3. 非目标(本期不做) - **手机验证码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示 + > **注意**:本条已更新。手机验证码登录已升级为 **MVP 正式功能**(见 Story 5),与密码登录并列提供。此非目标条目保留仅作版本记录,已失效。 - **微信扫码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示 - **单点登录(SSO)/ 企业微信集成**:后续版本规划 - **多设备并发登录的强制踢出策略**:本期允许同账号多端登录,后续安全策略模块规划 - **登录时段限制 / IP 白名单**:安全策略模块另行规划 -- **管理后台(Platform Admin)登录**:系统管理员登录管理后台的流程属于系统管理模块,本 PRD 专注租户内用户登录 +- **管理后台(Platform Admin)登录**:Tenant Admin(租户管理员)登录管理后台的流程属于系统管理模块,本 PRD 专注租户内用户登录 --- @@ -72,31 +73,33 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架 ### Story 1:新用户首次启动客户端——Tenant 识别 -**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 Tenant ID 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。 +**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 12位 Tenant Code 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。 **验收标准**: -- [ ] 客户端首次启动时(本地无 Tenant ID 缓存),自动呈现「Tenant 识别」界面,而非直接显示登录界面 -- [ ] 界面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant ID 输入框、「确认」按钮 -- [ ] Tenant ID 输入框支持粘贴操作,自动去除前后空格 +- [ ] 客户端首次启动时(本地无 Tenant Code 缓存),自动呈现「Tenant 识别」界面,而非直接显示登录界面 +- [ ] 界面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant Code 输入框、「确认」按钮 +- [ ] Tenant Code 输入框支持粘贴操作,自动去除前后空格 - [ ] 点击「确认」后,客户端向服务端发起 Tenant 验证请求(`POST /api/auth/tenant/verify/`),展示加载状态(spinner) -- [ ] **验证成功**:服务端返回租户名称及品牌信息(如公司名称、Logo URL);客户端将 Tenant ID 写入本地持久化存储,自动跳转至该租户的登录界面;界面顶部展示「正在登录:XX 房产」 -- [ ] **验证失败(Tenant ID 无效)**:输入框下方显示红色错误提示「识别码无效,请联系您的系统管理员获取正确的识别码」;Tenant ID 不写入本地缓存;用户可重新输入 +- [ ] **验证成功**:服务端返回租户名称及品牌信息(如公司名称、Logo URL);客户端将 Tenant Code 写入本地持久化存储,自动跳转至该租户的登录界面;界面顶部展示「正在登录:XX 房产」 +- [ ] **验证失败(Tenant Code 无效)**:输入框下方显示红色错误提示「识别码无效,请联系您的Tenant Admin(租户管理员)获取正确的识别码」;Tenant Code 不写入本地缓存;用户可重新输入 - [ ] **网络异常**:显示「网络连接失败,请检查网络后重试」,提供「重试」按钮 -- [ ] 非首次启动(本地已有合法 Tenant ID 缓存):直接跳过识别界面,进入登录界面 -- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA),点击后清除本地 Tenant ID 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」 +- [ ] 非首次启动(本地已有合法 Tenant Code 缓存):直接跳过识别界面,进入登录界面 +- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA),点击后清除本地 Tenant Code 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」 - [ ] Tenant 验证接口属于公开接口,无需鉴权;但需对单 IP 请求频率限制(每分钟 ≤ 10 次)以防止枚举攻击 --- -### Story 2:经纪人通过账号密码登录 +### Story 2:经纪人通过手机号和密码登录 -**As** 已识别租户的经纪人,**I want** 通过用户名和密码完成登录,**So that** 进入系统开始工作。 +**As** 已识别租户的经纪人,**I want** 通过手机号和密码完成登录,**So that** 进入系统开始工作。 + +> **说明**:普通员工的登录账号即为其手机号(由Tenant Admin(租户管理员)在新增员工时自动创建),无需记忆额外用户名。Tenant Admin 账号的登录名为平台运营自定义字符串,不受此约束。 **验收标准**: -- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、用户名输入框、密码输入框、滑块拼图验证区域、「登录」按钮 -- [ ] 用户名输入框 Placeholder:「请输入用户名」;支持英文字母、数字、下划线,最大长度 50 字符 +- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、手机号输入框、密码输入框、滑块拼图验证区域、「登录」按钮 +- [ ] 手机号输入框 Placeholder:「请输入您的手机号」;仅接受数字字符(非数字自动过滤),固定 11 位 - [ ] 密码输入框默认密文显示,右侧提供「显示/隐藏」图标切换明密文 - [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符,操作直观快速 - [ ] 验证逻辑:前端记录滑动轨迹(坐标序列 + 耗时),与背景图缺口位置一同发送至服务端;服务端综合校验**位置偏差**(允许 ±5px 容差)和**轨迹特征**(是否存在人类滑动的加速/减速规律)以区分机器行为 @@ -104,92 +107,121 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架 - [ ] 验证成功后,拼图区域显示绿色对勾 + 「验证通过」文案,状态持续至本次登录提交完成 - [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图(针对图片模糊或缺口不清晰的情况) - [ ] 背景图从预置图库中随机抽取,缺口位置每次随机生成,防止固定模式被预测 -- [ ] 三项(用户名、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰) +- [ ] 三项(手机号、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰) - [ ] 点击「登录」触发前端格式校验: - - 用户名为空 → 输入框下方红色提示「请输入用户名」 + - 手机号为空 → 输入框下方红色提示「请输入手机号」 + - 手机号不满 11 位 → 提示「请输入完整的 11 位手机号」 - 密码为空 → 提示「请输入密码」 - - 验证码为空 → 提示「请输入验证码」 + - 验证码为空 → 提示「请完成滑块验证」 - [ ] 格式校验通过后,向服务端发起登录请求,按钮进入 loading 状态防止重复提交 -- [ ] **登录成功**:服务端返回 Session Token;客户端存储 Token;跳转至系统首页;顶部显示欢迎信息「欢迎回来,{姓名}」 -- [ ] **登录失败(用户名或密码错误)**:显示「用户名或密码错误,请重新输入」(不区分是用户名错误还是密码错误,防止枚举攻击);验证码自动刷新;密码输入框清空;用户名保留 +- [ ] **登录成功(常规)**:服务端返回 Session Token 及 `is_initial_password` 标记;客户端存储 Token; + - 若 `is_initial_password = False`:直接跳转系统首页,顶部显示欢迎信息「欢迎回来,{姓名}」 + - 若 `is_initial_password = True`:**立即跳转「修改初始密码」强制页面**,不可关闭、不可跳过、不可访问任何其他功能页面(详见 §5.3.4) +- [ ] **登录失败(手机号或密码错误)**:显示「手机号或密码错误,请重新输入」(不区分具体原因,防止枚举攻击);验证码自动刷新;密码输入框清空;手机号保留 - [ ] **登录失败(验证码错误)**:显示「验证码有误,请重新输入」;验证码自动刷新;验证码输入框清空 - [ ] **账号被锁定**(同一账号密码连续错误 ≥ 5 次):显示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」;锁定状态下「登录」按钮置灰 - [ ] **账号已停用**:显示「账号已停用,请联系您的管理员」 - [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录界面,并提示「登录已过期,请重新登录」 -- [ ] 登录界面底部提供:「忘记用户名」链接、「忘记密码」链接(详见 Story 3、Story 4) +- [ ] 登录界面底部提供:「忘记密码」链接(详见 Story 3);移除「忘记用户名」入口(普通员工用户名即手机号,无需找回;Tenant Admin 如忘记用户名请联系平台运营) --- -### Story 3:经纪人找回用户名 +### Story 3:经纪人找回密码 -**As** 忘记用户名的经纪人,**I want** 通过绑定的邮箱或手机号找回用户名,**So that** 不依赖管理员也能自助恢复登录。 +**As** 忘记密码的经纪人,**I want** 通过手机号 + 短信验证码完成身份核验,重新设定密码,**So that** 无需邮箱、无需联系管理员,独立完成密码重置。 + +> **说明**:考虑到大多数Agent(经纪人)没有常用邮箱,本期找回密码统一通过短信验证码实现,废弃邮箱找回方式。账号中 `email` 字段在本系统无任何必须业务用途,完全可选。 **验收标准**: -- [ ] 点击登录界面「忘记用户名」链接,跳转至「找回用户名」页面(或弹窗) -- [ ] 找回方式(本期以邮箱为主,手机号为预留字段): - - 邮箱找回:输入注册邮箱,系统校验邮箱是否与已知账号匹配,匹配成功则发送包含用户名的邮件至该邮箱 - - 手机号找回(预留,UI 入口以「即将开放」禁用态展示) -- [ ] 邮箱输入框:格式校验(包含「@」和域名),错误时提示「请输入有效的邮箱地址」 -- [ ] 点击「发送」后: - - 邮箱存在且已绑定账号 → 显示「用户名已发送至您的邮箱,请查收」;发送按钮进入 60 秒倒计时不可重复点击 - - 邮箱不存在 → **不提示「邮箱未注册」**(防止用户信息枚举),统一显示「如该邮箱已绑定账号,您将收到一封包含用户名的邮件」 -- [ ] 邮件内容:纯文本邮件,包含用户名、发送时间,及「如非本人操作请联系管理员」说明 -- [ ] 发送频率限制:同一邮箱 1 小时内最多发送 3 次 -- [ ] 提供「返回登录」链接 +- [ ] 点击登录界面「忘记密码」链接,跳转至「找回密码」流程(Stepper 分步页面,共三步) + +**步骤一:输入手机号** + +- [ ] 页面显示:手机号输入框(11 位数字,自动过滤非数字)、「获取验证码」按钮、「返回登录」链接 +- [ ] 手机号为空或不足 11 位 → 点击「获取验证码」时在输入框下方提示「请输入完整的 11 位手机号」 +- [ ] 手机号格式合法后,点击「获取验证码」,按钮进入 60 秒倒计时冷却态(「重新获取(59s)」),倒计时结束后按钮恢复可点击 +- [ ] 服务端收到请求后: + - 若该手机号**存在**且账号状态为 `active`:向该号码发送 6 位数字短信验证码,有效期 **10 分钟** + - 若手机号**不存在**或账号已停用:页面统一提示「如该手机号已注册,验证码将在 1 分钟内发送」(**不泄露账号是否存在**) +- [ ] 同一手机号 1 小时内最多发送 **5 次**短信验证码,超限后提示「发送次数过多,请 1 小时后再试」 +- [ ] 短信内容模板:「【Fonrey 房睿】您的密码重置验证码为 {code},10 分钟内有效,请勿泄露。」 + +**步骤二:输入短信验证码** + +- [ ] 页面显示:6 位验证码输入框(支持分格输入)、「重新发送」倒计时链接、「下一步」按钮 +- [ ] 「下一步」按钮:6 位验证码全部输入后方可点击 +- [ ] 服务端校验验证码: + - 正确且未过期 → 进入步骤三,颁发一次性 `sms_reset_token`(有效期 15 分钟,一次性,服务端存储) + - 错误 → 提示「验证码有误,请重新输入」,错误次数 ≥ 5 次则本次验证码作废,需重新获取 + - 已过期 → 提示「验证码已过期,请重新获取」 + +**步骤三:重置密码** + +- [ ] 步骤三依赖步骤二颁发的 `sms_reset_token`(通过 URL 参数或会话状态传递),Token 无效或过期 → 显示「操作已超时,请重新发起找回密码」,跳回步骤一 +- [ ] **本页面复用「设置新密码」公共组件**(与首次登录强制修改密码页面为同一组件,详见 §5.3.4),保持 UI 与交互逻辑完全一致;入口上下文不同时,仅页面标题和提示文案有所差异: + +| 元素 | 首次登录强制修改(§5.3.4) | 找回密码步骤三(本 Story) | +|------|--------------------------|--------------------------| +| 页面标题 | 「欢迎使用 Fonrey,请先设置您的登录密码」 | 「重置您的登录密码」 | +| 提示文案 | 「您当前使用的是初始密码,为保障账号安全,请立即设置新密码后开始使用」 | 「请输入您的新密码,设置完成后请使用新密码重新登录」 | +| 提交按钮文案 | 「确认并进入系统」 | 「确认重置密码」 | +| 提交后跳转 | `is_initial_password = False`,Session 保持,直接进入首页 | 所有 Session 立即失效,跳转登录界面并提示「密码已重置,请使用新密码登录」 | + +- [ ] 提交成功后:`is_initial_password` 置为 **`False`**(找回密码属于用户主动操作,已完成身份核验,无需再触发强制修改流程) + > **注意**:与首次登录流程不同,找回密码时用户已通过短信验证码完成了身份核验,本次密码设置即视为"用户本人主动设置",不应再触发 `is_initial_password = True` 的二次强制修改。 --- -### Story 4:经纪人找回密码 +### Story 4:经纪人找回用户名(已废弃) -**As** 忘记密码的经纪人,**I want** 通过已知用户名 + 绑定邮箱(或手机号)自助重置密码,**So that** 不依赖管理员也能快速恢复登录。 +> **状态**:已废弃。普通员工用户名固定为手机号,无需找回;Tenant Admin 如忘记用户名,请联系平台运营线下处理。本 Story 保留占位以维持版本记录,实现时跳过此 Story。 + +--- + +### Story 5:手机验证码登录(MVP 实现) + +**As** 已有账号的经纪人,**I want** 通过手机号 + 短信验证码直接登录,**So that** 在忘记密码或不想输入密码时,仍能快速进入系统。 + +> **说明**:短信基础设施(`sms_otp_records` 表、OTP 发送/校验逻辑)已在 Story 3 找回密码中建设完成,本 Story 直接复用,实现成本极低。登录界面提供「密码登录」和「验证码登录」两个并列入口,用户自由切换,两种方式均为 MVP 正式功能。 **验收标准**: -- [ ] 点击登录界面「忘记密码」链接,跳转至「找回密码」流程(分步骤页面或 Stepper 组件) +- [ ] 登录界面提供两种登录方式的切换 Tab:**「密码登录」**(默认选中)和 **「验证码登录」** +- [ ] 切换 Tab 时,输入区域平滑切换,已填内容清空,滑块验证状态重置 -**步骤一:身份验证** +**「验证码登录」界面元素**: +- [ ] 手机号输入框(规格同 Story 2,11 位数字,自动过滤非数字) +- [ ] 验证码输入框(6 位数字分格输入)+ 「获取验证码」按钮(60 秒倒计时冷却态) +- [ ] 滑块拼图验证区域(规格同 Story 2,**先通过滑块验证,再允许点击「获取验证码」**) +- [ ] 「登录」按钮(手机号 + 验证码均填写后方可点击) -- [ ] 用户输入:用户名 + 邮箱(本期);手机号找回为预留入口(禁用态) -- [ ] 服务端校验用户名与邮箱是否匹配,不泄露具体原因(统一提示「如信息匹配,重置链接将发送至您的邮箱」) -- [ ] 校验通过后,向绑定邮箱发送含一次性重置链接的邮件;链接有效期 **30 分钟**,使用后立即失效 -- [ ] 同一账号 1 小时内最多发送 3 次重置邮件 +**获取验证码逻辑**: +- [ ] 用户须先完成滑块验证,「获取验证码」按钮方可点击;未完成滑块时点击 → 提示「请先完成滑块验证」 +- [ ] 点击「获取验证码」后,服务端: + - 手机号格式不合法 → 前端拦截,提示「请输入完整的 11 位手机号」 + - 手机号存在且状态 `active` → 发送 6 位 OTP,有效期 **5 分钟**,存入 `sms_otp_records`(`scene = 'login'`) + - 手机号不存在或已停用 → 统一响应「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举攻击) +- [ ] 同一手机号 1 小时内最多发送 **10 次**登录验证码(找回密码为独立计数,两者不共享限额);超限后提示「发送次数过多,请 1 小时后再试」 +- [ ] 短信内容模板:「【Fonrey 房睿】您的登录验证码为 {code},5 分钟内有效,请勿泄露。」 -**步骤二:重置密码** +**登录校验逻辑**: +- [ ] 点击「登录」,服务端校验 OTP: + - 正确且未过期 → 登录成功,后续行为与 Story 2 密码登录完全一致(含 `is_initial_password` 判断) + - 错误 → 提示「验证码有误,请重新输入」;连续错误 ≥ 5 次 → 本次 OTP 作废,提示「验证码已失效,请重新获取」 + - 已过期 → 提示「验证码已过期,请重新获取」 +- [ ] **账号被锁定**(密码登录失败次数触发):验证码登录仍受账号锁定限制,锁定期间无法通过任何方式登录,提示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」 + > **设计说明**:账号锁定是账号维度的安全策略,不区分登录方式;否则锁定形同虚设。 +- [ ] **账号已停用**:提示「账号已停用,请联系您的管理员」 -- [ ] 用户点击邮件中的链接,跳转至「重置密码」页面(链接含加密 Token,服务端校验 Token 有效性) -- [ ] Token 无效或已过期 → 显示「链接已过期或已使用,请重新申请」,提供「重新申请」按钮 -- [ ] 页面包含:新密码输入框、确认新密码输入框 -- [ ] **密码复杂度规则**(符合安全基线): - - 长度 8 ~ 32 位 - - 必须包含字母(区分大小写)和数字 - - 建议包含特殊符号(非强制,但页面提示推荐) - - 不得与最近 3 次历史密码相同 -- [ ] 两次密码输入不一致 → 提示「两次密码输入不一致」 -- [ ] 不符合复杂度 → 实时提示具体不满足的规则(逐条校验,红色 × / 绿色 ✓ 视觉指引) -- [ ] 提交成功 → 显示「密码已重置,请使用新密码登录」,自动跳转至登录界面;原所有 Session 立即失效(强制重新登录) - ---- - -### Story 5:预留——手机验证码登录(接口预留,v2 实现) - -**As** 绑定了手机号的经纪人,**I want** 通过手机号 + 短信验证码快速登录,**So that** 在忘记密码时仍能正常登录系统。 - -**当前状态**:本期 UI 入口以「即将开放」禁用态展示于登录界面,接口定义预留,不开放实际功能。 - -**预留接口设计**(供后端提前规划): +**接口规范**: ``` POST /api/auth/login/phone/ -Request: { phone: string, sms_code: string, tenant_id: string } -Response: { token: string, user: {...} } | { error: string } +Request: { phone: string, sms_code: string } +Response: { token: string, is_initial_password: bool, user: {...} } | { error_code: string, message: string } ``` -**绑定条件**(v2 实现时的前置要求): -- 手机号必须先在「个人设置」中与用户名账号完成绑定并通过验证 -- 一个手机号只能绑定一个用户名账号(同一租户内) -- 绑定手机号后,可通过手机号 + 短信验证码联合登录 - --- ### Story 6:预留——微信扫码登录(接口预留,v2 实现) @@ -221,9 +253,9 @@ POST /api/auth/wechat/callback/ # 微信扫码确认后回调,换取系 ``` 客户端启动 │ - ├─ 本地有 Tenant ID 缓存? + ├─ 本地有 Tenant Code 缓存? │ │ - │ YES ──→ 校验缓存 Tenant ID 是否仍有效(服务端 validate) + │ YES ──→ 校验缓存 Tenant Code 是否仍有效(服务端 validate) │ │ │ 有效 ──→ 直接进入登录界面 │ │ @@ -231,33 +263,33 @@ POST /api/auth/wechat/callback/ # 微信扫码确认后回调,换取系 │ └─ NO ──→ 显示 Tenant 识别界面 │ - 用户输入 Tenant ID → 发起验证 + 用户输入 Tenant Code → 发起验证 │ - 验证成功 ──→ 缓存 Tenant ID → 进入登录界面 + 验证成功 ──→ 缓存 Tenant Code → 进入登录界面 │ 验证失败 ──→ 显示错误信息,保持识别界面 ``` #### 5.1.2 Tenant 识别界面规范 -| 元素 | 规格 | -|------|------| -| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) | -| Logo | Fonrey 产品 Logo,居中显示 | -| 标题 | 「欢迎使用 Fonrey 房睿」 | -| 副标题 | 「请输入您公司的专属识别码以继续」 | -| Tenant ID 输入框 | 单行数字输入,固定 12 位,支持粘贴;非数字字符自动过滤,超出 12 位截断 | -| 输入框 Label | 「公司识别码(Tenant ID)」 | -| 确认按钮 | 主色调按钮,文字「确认」 | -| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) | -| 帮助文案 | 「不知道识别码?请联系您公司的系统管理员」 | +| 元素 | 规格 | +| ------------- | --------------------------------------- | +| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) | +| Logo | Fonrey 产品 Logo,居中显示 | +| 标题 | 「欢迎使用 Fonrey 房睿」 | +| 副标题 | 「请输入您公司的专属识别码以继续」 | +| Tenant Code 输入框 | 单行数字输入,固定 12 位,支持粘贴;非数字字符自动过滤,超出 12 位截断 | +| 输入框 Label | 「公司识别码(Tenant Code)」 | +| 确认按钮 | 主色调按钮,文字「确认」 | +| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) | +| 帮助文案 | 「不知道识别码?请联系您公司的Tenant Admin(租户管理员)」 | -#### 5.1.3 Tenant ID 格式规范 +#### 5.1.3 Tenant Code 格式规范 - **格式**:固定 **12 位纯数字**,如 `202500010001` - **生成规则**(建议):由平台运营在系统管理后台开通租户时自动生成,不允许手动指定,确保全局唯一性;可采用时间戳前缀 + 随机后缀的方式生成(如 `YYYYMM` + 6 位随机数) - **前端校验**:输入框仅接受数字字符(非数字自动过滤),输入满 12 位后自动触发格式完成状态;少于 12 位时点击「确认」弹出提示「识别码须为 12 位数字」 -- **唯一性**:全局唯一(公共 Schema 层面),同一 Tenant ID 不可分配给多个租户 +- **唯一性**:全局唯一(公共 Schema 层面),同一 Tenant Code 不可分配给多个租户 - **客户端存储**:Electron `app.getPath('userData')` 目录下的配置文件(加密存储,防止明文读取) #### 5.1.4 服务端 Tenant 验证接口规范 @@ -267,7 +299,7 @@ POST /api/auth/tenant/verify/ Request Body: { - "tenant_id": "202500010001" + "tenant_code": "202500010001" } Response 200 (成功): @@ -294,31 +326,48 @@ Response 200 (失败): #### 5.2.1 界面布局 +登录界面顶部以 **Tab 切换**区分两种登录方式(「密码登录」默认选中),Tab 下方的表单区随当前选中 Tab 动态切换,微信扫码作为独立的「其他登录」保持禁用。 + ``` ┌─────────────────────────────────────────┐ │ [租户 Logo] [租户公司名称] │ ← 顶部品牌区(Tenant 识别后回填) │ │ +│ ┌──────────────┬──────────────────┐ │ +│ │ 密码登录 ✓ │ 验证码登录 │ │ ← 登录方式 Tab,默认选中「密码登录」 +│ └──────────────┴──────────────────┘ │ +│ │ +│ ── 密码登录 Tab(默认展示)─────────── │ │ ┌───────────────────────┐ │ -│ │ 用户名 │ │ +│ │ 手机号 │ │ │ └───────────────────────┘ │ │ ┌───────────────────────┐ │ │ │ 密码 👁 │ │ │ └───────────────────────┘ │ │ ┌─────────────────────────────────┐ │ │ │ [背景图 + 拼图缺口] 🔄 │ │ ← 右上角刷新图标 -│ │ │ │ -│ │ [拼图碎片] │ │ -│ │ ├────────────────────────────── │ │ -│ │ ◀ 拖动滑块完成拼图 ▶ │ │ +│ │ [拼图碎片] ◀ 拖动滑块完成拼图 ▶│ │ │ └─────────────────────────────────┘ │ │ ┌───────────────────────┐ │ │ │ 登 录 │ │ ← 主 CTA,橙色 │ └───────────────────────┘ │ +│ 忘记密码 │ ← 文字链接(忘记用户名入口已废弃) │ │ -│ 忘记用户名 忘记密码 │ ← 次级入口,文字链接 +│ ── 验证码登录 Tab(切换后展示)──────── │ +│ ┌───────────────────────┐ │ +│ │ 手机号 │ │ +│ └───────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ [拼图验证] ← 先完成验证,再获取 │ │ ← 验证码登录下滑块前置 +│ └─────────────────────────────────┘ │ +│ ┌──────────────────┐ ┌─────────────┐ │ +│ │ 验证码(6 位) │ │ 获取验证码 │ │ ← 通过滑块后按钮可点击;60s 冷却 +│ └──────────────────┘ └─────────────┘ │ +│ ┌───────────────────────┐ │ +│ │ 登 录 │ │ +│ └───────────────────────┘ │ +│ 忘记密码 │ │ │ │ ─────────────── 其他登录 ──────────────│ -│ [手机验证码登录 - 即将开放] │ ← 禁用态,灰色 │ [微信扫码登录 - 即将开放] │ ← 禁用态,灰色 │ │ │ 切换公司 │ ← 底部,小字链接 @@ -335,7 +384,7 @@ Response 200 (失败): | 验证码有效期 | 单次验证会话有效,提交登录后服务端 Token 立即失效;超过 3 分钟未操作需重新加载 | | 验证失败处理 | 拼图区域抖动动画提示,自动刷新新背景图;**不计入账号密码错误次数**(行为验证失败属独立事件) | | 密码错误锁定 | 同一账号连续密码错误 ≥ 5 次,锁定 30 分钟;解锁方式:等待超时自动解锁 或 管理员手动解锁 | -| 密码错误计数 | 计数存于 Redis,Key 格式:`login_fail:tenant_id:username`,TTL 30 分钟 | +| 密码错误计数 | 计数存于 Redis,Key 格式:`login_fail:tenant_id:phone`(phone 即用户名/手机号),TTL 30 分钟 | | 验证码刷新 | 登录失败(用户名/密码错误)后自动刷新拼图;用户亦可主动点击「刷新」图标重新加载背景图 | | HTTPS | 所有登录相关请求强制 HTTPS,不允许 HTTP 降级 | | 密码传输 | 前端不做密码加密,HTTPS 层保证传输安全;后端存储使用 `django.contrib.auth` 默认的 `PBKDF2+SHA256` 哈希 | @@ -355,18 +404,19 @@ Response 200 (失败): 系统内共有两类账号创建场景,权限和规则各不相同: -**① Tenant Admin 账号(每个租户唯一的超级管理账号)** +**② Tenant Admin 账号(每个租户的超级管理账号)** -| 项目 | 规格 | -|------|------| -| 创建时机 | 平台运营在系统管理后台开通租户时,同步创建第一个 Tenant Admin 账号 | -| 用户名 | **由平台运营自定义设置**,格式:英文字母开头,仅含字母/数字/下划线,6~30 字符,同租户内唯一 | -| 初始密码 | **由平台运营自定义设置**,须符合密码复杂度规则(8~32 位,含字母+数字) | -| 首次登录 | 强制修改初始密码,不可跳过 | -| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 | -| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号(后续可扩展为多管理员,v2 规划) | +| 项目 | 规格 | +| ---- | ------------------------------------------------------------ | +| 创建时机 | 平台运营在系统管理后台开通租户时,系统**自动**以该租户联系人手机号创建 Tenant Admin 账号,无需手动设置 | +| 用户名 | **固定为该租户联系人的手机号**(11 位数字),全局唯一,创建后不可更改 | +| 初始密码 | **系统统一固定初始密码**(与普通员工相同,由平台在部署配置中设定,如 `Fonrey@2025`) | +| 首次登录 | 强制修改初始密码,不可跳过 | +| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 | +| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号(后续可扩展为多管理员,v2 规划) | +| 数据来源 | 联系人手机号来自 `public.tenants.contact_phone` 字段,开通租户时由平台运营录入,必填 | -**② 普通员工账号(经纪人、店长、行政等)** +**① 普通员工账号(经纪人、店长、行政等)** | 项目 | 规格 | |------|------| @@ -381,10 +431,10 @@ Response 200 (失败): | 字段 | 类型 | Tenant Admin | 普通员工账号 | 说明 | |------|------|-------------|-------------|------| -| 用户名(username) | CharField(30) | 平台运营自定义,字母开头,含字母/数字/下划线,6~30 字符 | **固定为员工手机号**(11 位数字) | 登录 ID,创建后不可更改 | -| 密码(password) | CharField | 平台运营自定义初始密码 | **系统统一固定初始密码** | PBKDF2+SHA256 哈希存储 | -| 手机号(phone) | CharField(11) | 选填,加密存储 | **必填,同时作为用户名**,加密存储,同租户内唯一 | 当前阶段为登录 ID;v2 启用手机验证码登录后复用此字段 | -| 邮箱(email) | EmailField | 选填,同租户唯一 | 选填,同租户唯一 | 用于找回密码;若为空则无法自助找回 | +| 用户名(username) | CharField(30) | **固定为联系人手机号**(11 位数字) | **固定为员工手机号**(11 位数字) | 登录 ID,创建后不可更改;两类账号规则统一 | +| 密码(password) | CharField | **系统统一固定初始密码** | **系统统一固定初始密码** | PBKDF2+SHA256 哈希存储;首次登录强制修改 | +| 手机号(phone) | CharField(11) | **必填,同时作为用户名**,来源于 `public.tenants.contact_phone` | **必填,同时作为用户名**,加密存储,同租户内唯一 | 两类账号均用手机号登录,v2 启用手机验证码后复用此字段 | +| 邮箱(email) | EmailField | 选填,同租户唯一 | 选填,同租户唯一 | 在本系统无必须业务用途,完全可选;普通员工忘记密码通过手机短信验证码自助找回,**与邮箱无关** | | 员工档案关联(staff_id) | OneToOneField → `org.Staff` | 可选关联(平台运营账号) | 必须关联 | 实名绑定 | | 账号状态(status) | CharField | `active` / `disabled` / `locked` | `active` / `disabled` / `locked` | locked 为密码错误锁定,30 分钟自动恢复 | | 初始密码标记(is_initial_password) | BooleanField | True(首次登录前) | True(首次登录前) | True 时登录成功后强制跳转修改密码页 | @@ -411,109 +461,84 @@ Response 200 (失败): --- -### 5.4 找回流程详细说明 +### 5.4 找回密码详细说明 -#### 5.4.1 找回用户名流程 +> **说明**:Story 4「找回用户名」已废弃。普通员工用户名固定为手机号,无需找回;Tenant Admin 如忘记用户名请联系平台运营线下处理。 -> **说明**:由于普通员工的用户名即为其**手机号**,通常无需「找回用户名」功能。登录界面的「忘记用户名」入口保留,但仅对 Tenant Admin 账号有意义(其用户名为自定义字符串)。 - -``` -用户点击「忘记用户名」 - │ - ├─ 普通员工:提示「您的登录账号为您的手机号,请直接使用手机号登录」 - │ 提供「返回登录」按钮 - │ - └─ Tenant Admin(用户名非手机号格式): - │ - ├─ 输入绑定邮箱 - │ │ - │ 服务端查询(不向前端返回查询结果,防止枚举) - │ │ - │ 统一响应「如该邮箱已绑定账号,您将收到邮件」 - │ │ - │ 后台:邮箱存在 → 发送邮件(包含用户名) - │ 邮箱不存在 → 静默处理 - │ - └─ 用户查收邮件,获取用户名 → 返回登录 -``` -![[找回用户名流程.png]] -> **前端识别逻辑**:用户在「忘记用户名」页面输入邮箱提交后,服务端根据是否匹配到 Tenant Admin 账号决定处理路径,前端无需区分,统一展示「如该邮箱已绑定账号,您将收到邮件」。 - -**邮件模板(找回用户名)**: - -``` -主题:您的 Fonrey 房睿用户名 - -您好, - -您请求找回在 [公司名称] 的 Fonrey 账号用户名。 - -您的用户名为:{username} - -如果这不是您的操作,请忽略此邮件。如有疑问,请联系您的系统管理员。 - -此邮件由系统自动发送,请勿回复。 -发送时间:{datetime} -``` - -#### 5.4.2 找回密码流程 +#### 5.4.1 找回密码流程 ``` 用户点击「忘记密码」 │ -步骤1:身份验证 +步骤1:输入手机号 │ - ├─ 输入手机号(即用户名)+ 绑定邮箱 - │ (Tenant Admin 则输入自定义用户名 + 绑定邮箱) + ├─ 输入 11 位手机号,点击「获取验证码」 │ │ - │ 服务端校验用户名与邮箱是否匹配 + │ 服务端校验手机号是否存在且状态为 active │ │ - │ 统一响应「如信息匹配,重置链接将发送至您的邮箱」(防止枚举) + │ 统一响应「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举) │ │ - │ 后台:匹配成功 → 生成加密 Token(有效期 30min)→ 异步发送邮件 - │ 不匹配 → 静默处理 + │ 后台:存在且 active → 生成 6 位 OTP,有效期 10 分钟,存入 sms_otp_records → 发送短信 + │ 不存在或已停用 → 静默处理 │ -步骤2:用户点击邮件中的重置链接 +步骤2:输入短信验证码 │ - ├─ 服务端校验 Token 有效性 + ├─ 输入 6 位验证码,点击「下一步」 │ │ - │ 有效 → 展示「重置密码」表单 + │ 服务端校验 OTP: │ │ - │ 无效/过期 → 提示「链接已过期,请重新申请」,提供「重新申请」按钮 + │ 正确且未过期 → 颁发一次性 sms_reset_token(有效期 15 分钟)→ 进入步骤3 + │ │ + │ 错误(累计 < 5 次)→ 提示「验证码有误,请重新输入」 + │ 错误(累计 ≥ 5 次)→ 提示「验证已失败,请重新获取验证码」,本次 OTP 作废 + │ 已过期 → 提示「验证码已过期,请重新获取」 │ -步骤3:用户输入并提交新密码 +步骤3:重置密码 │ - ├─ 密码复杂度校验(≥ 8 位,含字母+数字) - ├─ 与历史密码对比校验(最近 3 次,含固定初始密码) + ├─ 页面携带 sms_reset_token,服务端校验有效性 + │ │ + │ 无效/过期 → 提示「操作已超时,请重新发起找回密码」,跳回步骤1 + │ │ + ├─ 用户输入新密码 + 确认新密码,实时逐条校验复杂度规则(✓/✗) │ - └─ 校验通过 → 更新密码,is_initial_password = False - → 清除该账号所有有效 Session(强制重新登录) - → 跳转登录界面,提示「密码已重置,请重新登录」 -``` -![[找回密码流程.png]] -> **注意**:找回密码流程依赖员工账号绑定了邮箱。若员工未绑定邮箱,无法自助找回,需联系 Tenant Admin 在管理界面执行「重置密码」操作,将密码恢复为固定初始密码。 - -**邮件模板(重置密码)**: - -``` -主题:重置您的 Fonrey 房睿密码 - -您好, - -我们收到了重置您在 [公司名称] 的 Fonrey 账号密码的请求。 - -请点击以下链接重置密码(链接 30 分钟内有效): -{reset_link} - -如果您未发起此请求,请忽略此邮件,您的密码不会被更改。 - -此邮件由系统自动发送,请勿回复。 -发送时间:{datetime} + └─ 提交成功 + → 更新密码,is_initial_password = False + → 清除该账号所有有效 Session(强制重新登录) + → 跳转登录界面,提示「密码已重置,请使用新密码登录」 ``` --- -### 5.5 后端数据模型设计 +### 5.5 手机验证码登录详细说明 + +> 本节为 Story 5 的实现规范补充。短信基础设施(`sms_otp_records` 表、OTP 发送/校验逻辑)在 Story 3 找回密码中已建设完成,本节描述在**登录场景**下复用该基础设施时的关键差异点。 + +**与找回密码短信逻辑的差异对比**: + +| 维度 | 找回密码(Story 3) | 验证码登录(Story 5) | +|------|-------------------|---------------------| +| `scene` 字段 | `password_reset` | `login` | +| OTP 有效期 | 10 分钟 | 5 分钟 | +| 每小时发送上限 | 5 次 | 10 次 | +| 验证成功后动作 | 颁发 `sms_reset_token` → 步骤三重置密码 | 直接颁发 Session Token,登录成功 | +| 短信文案 | 「密码重置验证码」 | 「登录验证码」 | +| 账号锁定影响 | 不受密码错误锁定限制(非密码登录路径) | **受账号锁定限制**(账号维度安全策略,不区分方式)| + +**滑块验证前置规则**(验证码登录特有): + +- 用户须先完成滑块拼图验证,「获取验证码」按钮方可点击 +- 滑块验证通过后,拼图区域保持「验证通过」状态,不需要在点击「登录」前再次验证 +- 切换 Tab 时,滑块验证状态重置(须重新完成验证后方可获取验证码) + +**`sms_otp_records` 表复用说明**: + +- 不新建表,复用 `DATA_MODEL_LOGIN.md` 中定义的 `sms_otp_records` +- `scene` 字段区分场景:`login` / `password_reset`,各自独立限流计数 +- 同一手机号同一 scene 同一时间只有一条有效 OTP;新发送时将旧记录标记为 `used` + +--- + +### 5.6 后端数据模型设计 > **数据模型已迁移至独立文档**,请参阅: > **`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`** @@ -521,7 +546,7 @@ Response 200 (失败): 该文档包含: - `user_accounts` 账号主表(完整字段定义、约束、索引、Django Model 代码) - `login_attempts` 登录审计表 -- `password_reset_tokens` 密码重置令牌表 +- `sms_otp_records` 短信验证码记录表(找回密码 + 验证码登录共用) - `password_histories` 历史密码记录表 - Redis 缓存结构说明 - 账号状态机与创建流程 @@ -531,13 +556,13 @@ Response 200 (失败): --- -### 5.6 Electron 客户端登录相关约定 +### 5.7 Electron 客户端登录相关约定 | 约定项 | 规格 | |--------|------| -| Tenant ID 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密,不存储明文 | +| Tenant Code 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密,不存储明文 | | Session Token 存储 | 内存(`global` 变量)+ `session` Cookie(Chromium 管理),不写入磁盘明文文件 | -| 登录页加载 | 客户端主进程根据 Tenant ID 构建目标 URL(`https://{tenant_slug}.fonrey.com/auth/login/`),通过 `BrowserWindow.loadURL()` 加载 | +| 登录页加载 | 客户端主进程根据 Tenant Code 构建目标 URL(`https://{tenant_slug}.fonrey.com/auth/login/`),通过 `BrowserWindow.loadURL()` 加载 | | 多标签页处理 | 同一 `BrowserWindow` 内,所有页面共享同一 Session Cookie | | 客户端登出 | 调用服务端 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie | | 窗口关闭时 | Session 保留(不自动登出),下次打开客户端时若 Session 未过期,直接进入系统 | @@ -553,8 +578,9 @@ Response 200 (失败): |--------|------|------| | `django.contrib.auth` | 用户认证基础框架 | 扩展 `AbstractBaseUser` 而非直接使用 `User` 模型,以支持 `username` 唯一性约束在租户维度而非全局 | | `django-tenants` | 多租户隔离 | `UserAccount` 属于租户级 Schema,Tenant 验证接口属于 `shared_apps` | -| `Redis` | 滑块验证 Token 存储、登录失败计数、密码重置 Token 缓存 | 验证 Key:`captcha_token:{uuid}`(TTL 3min);登录失败 Key:`login_fail:{tenant_id}:{username}` | -| `Celery` | 发送找回邮件 | 邮件发送异步处理,防止接口响应超时 | +| `Redis` | 滑块验证 Token 存储、登录失败计数、短信 OTP 限流计数 | 验证 Key:`captcha_token:{uuid}`(TTL 3min);登录失败 Key:`login_fail:{tenant_id}:{username}`;OTP 限流 Key:`sms_limit:{scene}:{phone}`(TTL 1h)| +| 短信服务(待选型) | 发送登录验证码 / 找回密码验证码 | 国内需选用具备短信资质的服务商(如阿里云短信、腾讯云短信);需申请短信签名和模板审核 | +| `Celery` | 异步任务处理 | 短信发送异步处理,防止接口响应超时;原邮件发送需求已废弃,短信为主要通知方式 | | `django-ratelimit` 或自定义中间件 | 接口限流 | Tenant 验证接口、登录接口、找回密码接口均需限流 | | `Pillow` | 滑块拼图图片处理 | 生成拼图背景图(抠出缺口区域)及对应的拼图碎片图片,输出为 Base64,分别通过两个字段返回给前端 | @@ -569,17 +595,17 @@ Response 200 (失败): | 风险 | 可能性 | 影响 | 缓解措施 | |------|--------|------|---------| | 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征(非线性运动特征),拒绝匀速/程序化轨迹;后续可引入设备指纹加固 | -| Tenant ID 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流(每IP每分钟≤10次),返回结果不区分「未找到」与「已禁用」| -| 密码重置 Token 泄露 | 低 | 高 | Token 单次有效、30分钟过期、HTTPS 传输 | -| 邮件发送失败导致用户无法找回密码 | 中 | 中 | 邮件发送失败写入告警日志,管理员可通过后台查看 Token 手动告知用户 | +| Tenant Code 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流(每IP每分钟≤10次),返回结果不区分「未找到」与「已禁用」| +| 密码重置 Token 泄露 | 低 | 高 | `sms_reset_token` 单次有效、15 分钟过期、HTTPS 传输 | +| 短信服务故障导致用户无法找回密码或验证码登录 | 中 | 高 | 短信发送失败写入告警日志;密码登录作为保底方式(非单一入口);建议配置备用短信服务商通道 | | 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出,可在 Token 机制中引入版本号 | ### 6.4 开放问题(开发前需确认) -- [ ] **邮件服务商选型**:使用 SendGrid / 阿里云邮件推送 / SMTP 自建?需运维确认 — 负责人:后端负责人 — 截止:开发启动前 +- [ ] **短信服务商选型**:使用阿里云短信 / 腾讯云短信 / 其他服务商?需运维确认并提前申请短信签名和模板审核(国内审核周期 1–3 个工作日)— 负责人:后端负责人 — 截止:开发启动前 - [ ] **Session 有效期默认值**:8 小时是否满足各租户需求?是否允许租户管理员自行配置?— 负责人:产品经理 — 截止:开发启动前 - [ ] **滑块拼图实现方案**:自研(Pillow 生成图片 + 前端拖拽组件)还是集成第三方行为验证服务(如极验 GeeTest / 网易易盾)?自研可控但需维护图库;第三方开箱即用但引入外部依赖,需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前 -- [ ] **账号锁定通知**:账号被锁定后,是否自动发邮件通知用户和/或管理员?— 负责人:产品经理 — 截止:开发启动前 +- [ ] **账号锁定通知**:账号被锁定后,是否自动发短信通知用户和/或通知管理员(站内消息)?— 负责人:产品经理 — 截止:开发启动前 - [ ] **历史密码校验范围**:最近 3 次是否足够?是否需要额外规则(如不能与用户名相同)?— 负责人:产品经理 — 截止:开发启动前 --- @@ -602,7 +628,7 @@ Response 200 (失败): ``` [未识别 Tenant] - │ 输入有效 Tenant ID + │ 输入有效 Tenant Code ↓ [未登录] │ 账密登录成功 @@ -625,24 +651,20 @@ Response 200 (失败): | 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 说明 | |------|------|------------|------------|------| -| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | Tenant ID 验证 | +| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | Tenant Code 验证 | | `/api/auth/captcha/` | GET | Tenant | 否 | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token) | | `/api/auth/captcha/verify/` | POST | Tenant | 否 | 提交滑动轨迹 + 位置,服务端校验并返回一次性通过凭证(供登录接口使用) | -| `/api/auth/login/` | POST | Tenant | 否 | 账号密码登录 | +| `/api/auth/login/` | POST | Tenant | 否 | 手机号 + 密码登录 | +| `/api/auth/login/phone/` | POST | Tenant | 否 | 手机号 + 短信验证码登录(MVP 正式功能) | | `/api/auth/logout/` | POST | Tenant | 是 | 登出,使 Session 失效 | -| `/api/auth/recover/username/` | POST | Tenant | 否 | 发起找回用户名 | -| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送邮件) | +| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送短信验证码) | +| `/api/auth/recover/password/verify/` | POST | Tenant | 否 | 校验短信验证码,颁发一次性 `sms_reset_token` | | `/api/auth/recover/password/reset/` | POST | Tenant | 否(Token 鉴权) | 提交新密码 | -| `/api/auth/login/phone/` | POST | Tenant | 否 | **预留**,手机验证码登录 | -| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | **预留**,获取微信二维码 | -| `/api/auth/wechat/callback/` | POST | Tenant | 否 | **预留**,微信扫码回调 | +| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | **预留 v2**,获取微信二维码 | +| `/api/auth/wechat/callback/` | POST | Tenant | 否 | **预留 v2**,微信扫码回调 | ### 8.3 相关文档参考 -- 客户端发布管理模块 PRD:`Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md` -- 组织人事管理模块 PRD:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` -- 权限管理模块 PRD:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` -- 系统管理模块 PRD:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 技术栈文档:`Project/fonrey/TECH_STACK/TECH_STACK.md` - **登录管理数据模型**:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` - **登录管理技术方案**:`Project/fonrey/TECH_STACK/登录管理技术方案.md` diff --git a/Project/fonrey/PRD/系统管理/系统管理模块PRD.md b/Project/fonrey/PRD/系统管理/系统管理模块PRD.md index 233086d6..29c46fd4 100644 --- a/Project/fonrey/PRD/系统管理/系统管理模块PRD.md +++ b/Project/fonrey/PRD/系统管理/系统管理模块PRD.md @@ -33,7 +33,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten | 角色 | 使用场景 | 频率 | | --------------------------- | ----------- | ------ | -| 超级管理员(Platform Super Admin) | 全局配置、高危操作授权 | 低频(每周) | +| Platform Admin(平台超级管理员)(Platform Super Admin) | 全局配置、高危操作授权 | 低频(每周) | | 运维人员(Ops Operator) | 日常租户管理、监控巡检 | 高频(每日) | | 只读审计员(Read-only Auditor) | 日志查询、合规报告导出 | 中频(每周) | @@ -97,13 +97,13 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten --- -### Persona B:超级管理员 David(系统升级与回滚) +### Persona B:Platform Admin(平台超级管理员) David(系统升级与回滚) > 负责平台技术运维,周期性执行版本升级,关注升级稳定性与租户影响面,有权执行所有高危操作。 **Story 4**:灰度系统升级 -> 作为超级管理员,我希望先对内测租户升级新版本,验证稳定后再全量推送,避免一次性影响所有客户。 +> 作为Platform Admin(平台超级管理员),我希望先对内测租户升级新版本,验证稳定后再全量推送,避免一次性影响所有客户。 **验收标准**: - [ ] 升级前自动执行健康检查,存在异常服务时阻断升级并提示 @@ -113,7 +113,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten **Story 5**:升级失败回滚 -> 作为超级管理员,我希望在升级出现问题时能立即回滚至上一稳定版本,并生成事件报告。 +> 作为Platform Admin(平台超级管理员),我希望在升级出现问题时能立即回滚至上一稳定版本,并生成事件报告。 **验收标准**: - [ ] 回滚操作触发前自动保存当前状态快照 @@ -177,7 +177,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten | 模式 | 说明 | |------|------| | 软删除(Soft Delete) | 标记删除状态,数据保留 30 天(默认,可配置)后由 Celery 定时任务清除 | -| 硬删除(Hard Delete) | 立即清除所有数据、Schema、存储资源及子域名授权;仅超级管理员可操作 | +| 硬删除(Hard Delete) | 立即清除所有数据、Schema、存储资源及子域名授权;仅Platform Admin(平台超级管理员)可操作 | 删除前置条件: 1. 操作人必须确认数据导出已完成(勾选确认框) @@ -264,7 +264,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten **Tenant Admin 管理** - 每个租户可设置 1 至多名 Tenant Admin(超级用户) -- 平台管理员可直接在后台创建新用户并赋予 Tenant Admin 角色,或从租户现有用户中指定 +- Platform Admin(平台超级管理员)可直接在后台创建新用户并赋予 Tenant Admin 角色,或从租户现有用户中指定 - 支持查看当前 Tenant Admin 列表,执行:新增 / 替换 / 撤销权限 **Tenant Admin 权限配置(RBAC)** @@ -282,7 +282,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten **密码重置** -- 平台管理员可为任意租户的任意用户发起密码重置 +- Platform Admin(平台超级管理员)可为任意租户的任意用户发起密码重置 - 方式一:发送重置链接至注册邮箱(用户自助重置) - 方式二:管理员直接设置临时密码(用户首次登录后强制修改) - 所有重置操作记录于操作审计日志 @@ -331,7 +331,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten **灰度升级策略**: -- 维护"内测租户组"列表,由超级管理员配置 +- 维护"内测租户组"列表,由Platform Admin(平台超级管理员)配置 - 灰度阶段仅对内测租户执行升级,其余租户保持原版本 - 内测租户验证通过(手动确认)后,触发全量升级 @@ -423,7 +423,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten **管理员设置** - 管理员账号管理(创建、编辑、停用) -- 角色配置(超级管理员 / 运营人员 / 只读审计员) +- 角色配置(Platform Admin(平台超级管理员) / 运营人员 / 只读审计员) - MFA 设置(强制启用,支持 TOTP) - IP 白名单配置 - 登录会话管理(查看活跃会话、强制登出) @@ -438,7 +438,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten | IP 白名单 | 仅允许指定 IP 范围访问管理控制台 URL(Nginx 层或应用层限制) | | 高危操作二次验证 | 删除租户、数据恢复、系统回滚操作触发 MFA 二次确认弹窗 | | 会话超时 | 无操作 30 分钟后自动登出,Token 失效 | -| 强制登出 | 超级管理员可在"管理员设置"中强制终止指定管理员的所有会话 | +| 强制登出 | Platform Admin(平台超级管理员)可在"管理员设置"中强制终止指定管理员的所有会话 | **与租户应用隔离**: @@ -546,7 +546,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten ### 9.2 管理员角色权限矩阵 -| 操作 | 超级管理员 | 运营人员 | 只读审计员 | +| 操作 | Platform Admin(平台超级管理员) | 运营人员 | 只读审计员 | |------|-----------|---------|-----------| | 创建租户 | ✅ | ✅ | ❌ | | 挂起 / 恢复租户 | ✅ | ✅ | ❌ | diff --git a/Project/fonrey/PRD/系统配置/系统配置参数数据.md b/Project/fonrey/PRD/系统配置/系统配置参数数据.md index ca387168..df12dc5d 100644 --- a/Project/fonrey/PRD/系统配置/系统配置参数数据.md +++ b/Project/fonrey/PRD/系统配置/系统配置参数数据.md @@ -277,7 +277,7 @@ | 相关方 | 相关方说明 | 状态 | 启动权限 | 操作 | | ------ | ------------------------------ | --- | ------------------------ | -------------- | -| 平台摄影师 | 开启平台实勘功能后,在房源预约拍摄完成后统一处理为系统管理员 | 停用 | | 权限配置 | +| 平台摄影师 | 开启平台实勘功能后,在房源预约拍摄完成后统一处理为Tenant Admin(租户管理员) | 停用 | | 权限配置 | | 维护人 | 出租或出售房源的维护人 | 停用 | | 权限配置 | | 售维护人 | 出售房源的维护人(租、售维护人分开时自动启用) | 停用 | | 权限配置 | | 租维护人 | 出租房源的维护人(租、售维护人分开时自动启用) | 停用 | | 权限配置 | diff --git a/Project/fonrey/PRD/系统配置/系统配置模块PRD.md b/Project/fonrey/PRD/系统配置/系统配置模块PRD.md index 6a743b25..37b5900d 100644 --- a/Project/fonrey/PRD/系统配置/系统配置模块PRD.md +++ b/Project/fonrey/PRD/系统配置/系统配置模块PRD.md @@ -50,12 +50,12 @@ ## 4. 目标用户 -**主要角色**:系统管理员(租户侧,每租户 1~3 人) +**主要角色**:Tenant Admin(租户管理员)(租户侧,每租户 1~3 人) > 典型画像:门店运营负责人或行政主管,熟悉业务流程,无技术背景,通过系统后台进行日常运营配置。使用频率:初始开通时高频(完成初始化配置),此后低频(按需调整)。 **间接受益角色**: -- 一线经纪人 — 看到的下拉选项和必填规则由管理员配置决定 +- Agent(经纪人) — 看到的下拉选项和必填规则由管理员配置决定 - 店长/经理 — 配置直接影响客源来源分析报表的数据质量 --- @@ -66,7 +66,7 @@ ### US-SETTING-001-A:管理员配置可选枚举值(Lookup Items) -> **As** 系统管理员, +> **As** Tenant Admin(租户管理员), > **I want** 在「系统设置 → 参数配置」页面维护各业务模块的下拉选项(如客源来源、跟进目的), > **So that** 经纪人录入时看到的选项符合公司实际业务,不再依赖研发修改代码。 @@ -117,7 +117,7 @@ ### US-SETTING-001-B:管理员配置房源字段必填规则 -> **As** 系统管理员, +> **As** Tenant Admin(租户管理员), > **I want** 按「房源用途 × 交易状态」的组合,控制哪些字段在录入时为必填/选填/隐藏, > **So that** 系统能在录入时强制采集公司要求的关键信息,提升房源数据完整度。 @@ -165,7 +165,7 @@ ### US-SETTING-001-C:管理员配置客源录入规则 -> **As** 系统管理员, +> **As** Tenant Admin(租户管理员), > **I want** 配置新增私客时的查重范围,以及必填字段控制, > **So that** 减少客源重复录入风险,并确保客源数据质量满足公司管理要求。 @@ -222,7 +222,7 @@ **核心设计决策**: -1. **Lookup Items 与 enum_labels 分离**:固定系统枚举(装修/朝向/状态/等级)存放在 Public Schema 的 `enum_labels` 表,由平台管理员通过 migration 维护,租户无权修改。可配置枚举(来源/跟进目的)存放在 Tenant Schema 的 `lookup_items` 表,由租户管理员自主维护。详见数据模型说明文档。 +1. **Lookup Items 与 enum_labels 分离**:固定系统枚举(装修/朝向/状态/等级)存放在 Public Schema 的 `enum_labels` 表,由Platform Admin(平台超级管理员)通过 migration 维护,租户无权修改。可配置枚举(来源/跟进目的)存放在 Tenant Schema 的 `lookup_items` 表,由租户管理员自主维护。详见数据模型说明文档。 2. **字段规则不新增字段**:`field_requirement_rules` 只控制「必填/选填/隐藏」状态,字段本身的存在性由数据模型决定。避免配置层与数据模型层职责混淆。 diff --git a/Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md b/Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md index 011533e2..c3e475da 100644 --- a/Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md +++ b/Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md @@ -25,9 +25,9 @@ | 角色 | 描述 | 使用频率 | |------|------|----------| -| 系统管理员 / HR 行政 | 负责新增/编辑部门、办理员工入职/离职/调岗,维护账号状态与证件信息 | 每日 | +| Tenant Admin(租户管理员) / Tenant Admin(租户管理员) | 负责新增/编辑部门、办理员工入职/离职/调岗,维护账号状态与证件信息 | 每日 | | 店长 / 区域经理 | 查看本部门组织架构、员工列表,发起入职邀请 | 每日 | -| 一线经纪人 | 查看同事联系方式(通讯录),查看自己的档案信息 | 按需 | +| Agent(经纪人) | 查看同事联系方式(通讯录),查看自己的档案信息 | 按需 | | 公司管理层 | 通过架构图了解全公司组织结构,监控人员异动动态 | 按需 | --- @@ -63,7 +63,7 @@ ### Story 1:管理员查看组织人员列表 -**As** 系统管理员/店长,**I want** 在组织结构页面查看公司所有部门及其员工信息,**So that** 能快速掌握当前人员分布并进行管理操作。 +**As** Tenant Admin(租户管理员)/店长,**I want** 在组织结构页面查看公司所有部门及其员工信息,**So that** 能快速掌握当前人员分布并进行管理操作。 **验收标准**: - [ ] 页面入口路径:顶部导航「人事」→「组织人事」→「组织结构」,面包屑显示「人事OA / 组织人事 / 组织结构」 @@ -72,7 +72,7 @@ - [ ] 点击部门节点后,右侧展示该部门及其下级部门员工列表(可通过「显示下属部门员工」下拉切换) - [ ] 右上角显示全局系统提示(账号数量上限、实名认证不匹配人数等),提示可点击「立即筛选数据」跳转至对应筛选结果 - [ ] 页面右上角有「员工入黑名单」快捷操作入口 -- [ ] 员工列表支持多条件筛选:姓名/工号/电话(文本搜索)、职务(下拉选择)、职务类别(全选/单选)、员工状态(下拉,含已选 N 个计数)、审批状态(下拉)、冻结状态(全选)、登录账号(全选)、系统管理员(请选择)、入职时间(日期范围)、离职时间(日期范围)、显示下属部门员工(显示/隐藏)、部门级别(全选)、证件状态(不限)、证件号搜索 +- [ ] 员工列表支持多条件筛选:姓名/工号/电话(文本搜索)、职务(下拉选择)、职务类别(全选/单选)、员工状态(下拉,含已选 N 个计数)、审批状态(下拉)、冻结状态(全选)、登录账号(全选)、Tenant Admin(租户管理员)(请选择)、入职时间(日期范围)、离职时间(日期范围)、显示下属部门员工(显示/隐藏)、部门级别(全选)、证件状态(不限)、证件号搜索 - [ ] 点击「查询」按钮执行筛选,点击「清空条件」重置所有筛选项 - [ ] 员工列表支持批量操作:勾选复选框后,可执行「批量调动员工」「批量设置员工上级」,通过「更多」下拉展开更多批量操作 - [ ] 列表操作区包含:「新增员工」(主按钮,带下拉箭头)、「导出员工」、「批量调动员工」、「批量设置员工上级」、「更多」、「员工异动记录」(链接) @@ -86,7 +86,7 @@ ### Story 2:管理员新增部门 -**As** 系统管理员,**I want** 新增一个业务部门并配置其基本信息,**So that** 新部门能纳入组织架构并支持员工归属。 +**As** Tenant Admin(租户管理员),**I want** 新增一个业务部门并配置其基本信息,**So that** 新部门能纳入组织架构并支持员工归属。 **验收标准**: - [ ] 点击左侧「+ 新增部门」按钮,跳转至「部门新增」页面,面包屑显示「人事OA / 组织人事 / 组织结构 / 部门新增」 @@ -113,7 +113,7 @@ ### Story 3:管理员编辑部门信息 -**As** 系统管理员,**I want** 编辑已有部门的基本信息,**So that** 组织信息保持最新准确状态。 +**As** Tenant Admin(租户管理员),**I want** 编辑已有部门的基本信息,**So that** 组织信息保持最新准确状态。 **验收标准**: - [ ] 通过部门详情页右上角「编辑」按钮进入「部门编辑」页面,面包屑显示「人事OA / 组织人事 / 组织结构 / 部门编辑」 @@ -163,7 +163,7 @@ ### Story 6:查看员工详情 - 员工基本信息 -**As** HR 管理员,**I want** 查看某员工的完整档案信息,**So that** 能全面了解员工的任职、个人、来源等情况。 +**As** Tenant Admin(租户管理员),**I want** 查看某员工的完整档案信息,**So that** 能全面了解员工的任职、个人、来源等情况。 **验收标准**: - [ ] 点击员工列表的「查看」操作进入员工详情页,页面标题显示「[部门名称] [员工姓名]」,面包屑显示「人事OA / 组织人事 / 组织结构 / 员工详情」 @@ -218,7 +218,7 @@ ### Story 7:查看员工详情 - 异动记录 -**As** HR 管理员,**I want** 在员工详情页查看该员工的所有人事异动历史,**So that** 能追溯员工入职、调岗、上级变动等完整轨迹。 +**As** Tenant Admin(租户管理员),**I want** 在员工详情页查看该员工的所有人事异动历史,**So that** 能追溯员工入职、调岗、上级变动等完整轨迹。 **验收标准**: - [ ] 在员工详情页左侧导航点击「异动记录」切换至异动记录 Tab @@ -233,7 +233,7 @@ ### Story 8:查看员工详情 - 账号信息 -**As** HR 管理员/系统管理员,**I want** 在员工详情页查看和管理该员工的系统账号及第三方平台账号,**So that** 能统一管理员工的登录凭证和外部账号绑定状态。 +**As** Tenant Admin(租户管理员)/Tenant Admin(租户管理员),**I want** 在员工详情页查看和管理该员工的系统账号及第三方平台账号,**So that** 能统一管理员工的登录凭证和外部账号绑定状态。 **验收标准**: - [ ] 在员工详情页左侧导航点击「账号信息」切换至账号信息 Tab @@ -280,7 +280,7 @@ ### Story 10:查看组织员工异动记录(全局视图) -**As** HR 管理员,**I want** 在组织结构模块查看全公司所有员工的异动记录汇总,**So that** 能统一审计和追踪所有人事变动。 +**As** Tenant Admin(租户管理员),**I want** 在组织结构模块查看全公司所有员工的异动记录汇总,**So that** 能统一审计和追踪所有人事变动。 **验收标准**: - [ ] 异动记录入口:组织结构员工列表页右上角「员工异动记录」链接,跳转至异动记录汇总页,面包屑显示「人事OA / 组织人事 / 组织结构 / 异动记录」 @@ -299,7 +299,7 @@ ### Story 11:员工离职操作 -**As** HR 管理员/店长,**I want** 在组织结构员工列表中对在职员工发起离职操作,**So that** 员工状态及时变更为「离职」,并触发业务数据的归属处理流程。 +**As** Tenant Admin(租户管理员)/店长,**I want** 在组织结构员工列表中对在职员工发起离职操作,**So that** 员工状态及时变更为「离职」,并触发业务数据的归属处理流程。 **验收标准**: - [ ] 离职操作入口:员工列表行右侧「异动」下拉菜单中点击「离职」,弹出「员工离职」对话框(Modal 形式,背景遮罩,不跳转页面) @@ -328,7 +328,7 @@ ### Story 12:员工调动操作 -**As** HR 管理员,**I want** 通过右侧抽屉面板对员工发起调动操作并修改其部门、上级、职务等信息,**So that** 员工的组织归属变更即时生效并留下完整的调动记录。 +**As** Tenant Admin(租户管理员),**I want** 通过右侧抽屉面板对员工发起调动操作并修改其部门、上级、职务等信息,**So that** 员工的组织归属变更即时生效并留下完整的调动记录。 **验收标准**: - [ ] 调动操作入口:员工列表行右侧「异动」下拉菜单中点击「调动」,从页面右侧滑出「员工调动」抽屉面板(不跳转页面,背景列表可见但交互禁用) @@ -367,7 +367,7 @@ ### Story 13:查看员工奖惩记录 -**As** HR 管理员/店长,**I want** 在员工详情页查看该员工的所有奖惩记录,**So that** 能了解员工的奖励与处罚历史,作为绩效管理和晋升的参考依据。 +**As** Tenant Admin(租户管理员)/店长,**I want** 在员工详情页查看该员工的所有奖惩记录,**So that** 能了解员工的奖励与处罚历史,作为绩效管理和晋升的参考依据。 **验收标准**: - [ ] 在员工详情页左侧导航点击「奖惩记录」切换至奖惩记录 Tab,当前选中项高亮(橙色文字 + 左侧橙色指示条) @@ -381,7 +381,7 @@ ### Story 14:新增员工奖惩记录 -**As** HR 管理员,**I want** 在员工奖惩记录页面新增一条奖惩记录,**So that** 员工的奖励或处罚情况被系统留档,可追溯查询。 +**As** Tenant Admin(租户管理员),**I want** 在员工奖惩记录页面新增一条奖惩记录,**So that** 员工的奖励或处罚情况被系统留档,可追溯查询。 **验收标准**: - [ ] 点击奖惩记录页面右上角「新增」按钮,弹出「新增奖惩记录」对话框(Modal 形式,标题「新增奖惩记录」,右上角有「×」关闭按钮) diff --git a/Project/fonrey/TECH_STACK/测试规范.md b/Project/fonrey/TECH_STACK/测试规范.md index 855fef91..8cf46455 100644 --- a/Project/fonrey/TECH_STACK/测试规范.md +++ b/Project/fonrey/TECH_STACK/测试规范.md @@ -2,11 +2,11 @@ # Fonrey 测试规范(TEST_SPEC) -**版本**: 1.1 +**版本**: 1.2 **项目**: Fonrey 房产经纪管理系统 **技术栈**: Django 4.x + django-tenants + PostgreSQL 16 + Redis + Celery + HTMX + Playwright -**关联文档**: `TECH_STACK/TECH_STACK.md`、`PRD/TASK.md`、各模块技术方案(登录/权限/房源/客源/楼盘/组织人事/系统管理) -**最后更新**: 2026-04-27 +**关联文档**: `TECH_STACK/TECH_STACK.md`、`PRD/TASK.md`、`TEST_CASES/TEST_CASE_ID_SPEC.md`、`TEST_CASES/TEST_CASE_REGISTRY.md`、各模块技术方案(登录/权限/房源/客源/楼盘/组织人事/系统管理) +**最后更新**: 2026-04-30 --- @@ -36,13 +36,13 @@ Fonrey 采用 AI 驱动迭代,测试是质量兜底。所有 P0 User Story 必 | `apps/*/services/` 业务逻辑层 | ≥ 80% | | `apps/*/views*` 接口与视图层 | ≥ 70% | | `apps/*/tasks.py` 异步任务 | ≥ 70% | -| E2E 核心旅程 | 5 条全部通过 | +| E2E 核心测试用例 | 覆盖指定核心用例并全部通过 | ### 2.2 质量门禁 - 每个 P0 US 对应至少一个集成测试场景集。 - PR 合并前:单元 + 集成必须全绿。 -- `main/develop`:每日自动跑全量(含 E2E 核心旅程)。 +- `main/develop`:每日自动跑全量(含 E2E 核心测试用例)。 --- @@ -50,7 +50,7 @@ Fonrey 采用 AI 驱动迭代,测试是质量兜底。所有 P0 User Story 必 ``` ┌─────────────────────────────────────────┐ -│ E2E 测试(用户旅程) │ ← Playwright +│ E2E 测试(测试用例) │ ← Playwright ├─────────────────────────────────────────┤ │ 集成测试(HTTP / View / Service / DB) │ ← pytest-django + TenantClient ├─────────────────────────────────────────┤ @@ -71,7 +71,7 @@ Fonrey 采用 AI 驱动迭代,测试是质量兜底。所有 P0 User Story 必 ### 3.3 E2E 测试 - 目标:验证真实用户关键路径。 -- 约束:只覆盖核心旅程,避免把所有细节都堆到 E2E。 +- 约束:只覆盖核心测试用例,避免把所有细节都堆到 E2E。 --- @@ -236,15 +236,12 @@ result = some_task.apply(args=[...]) ## 九、E2E 测试规范 -### 9.1 核心旅程(必须) +### 9.1 核心测试用例(必须) -| 编号 | 旅程 | 对应模块 | -|---|---|---| -| J-01 | 登录 → 进入首页 | 登录 | -| J-02 | 录入房源 → 上传图片 → 查看列表 | 房源 | -| J-03 | 录入客源 → 添加跟进 | 客源 | -| J-04 | 无权限访问受限页面 | 权限 | -| J-05 | 创建员工 → 分配角色 → 新员工登录 | 组织人事 + 权限 | +- E2E 覆盖对象采用“测试用例”定义,不使用“旅程编号(J-xx)”。 +- 每条 E2E 用例必须绑定全局唯一测试用例ID:`TC-FON-XXXXXX`。 +- 当前登录模块核心用例以 `TEST_CASES/TEST_CASES_LOGIN_MODULE.md` 为准(`TC-FON-000001` ~ `TC-FON-000048`)。 +- 其他模块(房源/客源/组织/权限等)按 `TEST_CASES/TEST_CASE_REGISTRY.md` 分配编号后补充。 ### 9.2 Playwright 约束 @@ -264,9 +261,31 @@ page.wait_for_load_state('networkidle') --- -## 十、测试配置基线 +## 十、测试用例编号与注册规范(强制) -### 10.1 `pytest.ini` +### 10.1 编号规范 + +- 测试用例ID:`TC-FON-XXXXXX`(全局唯一) +- 步骤ID:`TC-FON-XXXXXX-SYY` +- 详见:`TEST_CASES/TEST_CASE_ID_SPEC.md` + +### 10.2 注册流程 + +1. 新增用例前,先在 `TEST_CASES/TEST_CASE_REGISTRY.md` 查看下一个可用编号。 +2. 先登记编号段(可先 `reserved`),再编写文档和代码。 +3. 合并前状态改为 `active`,并更新“当前编号水位”。 + +### 10.3 强约束 + +- 不允许按模块重置编号。 +- 不允许复用已废弃编号。 +- 不允许未登记编号直接入库测试代码。 + +--- + +## 十一、测试配置基线 + +### 11.1 `pytest.ini` ```ini [pytest] @@ -280,7 +299,7 @@ markers = slow ``` -### 10.2 `tests/settings_test.py` 关键项 +### 11.2 `tests/settings_test.py` 关键项 - Celery eager 模式开启 - Cache 使用测试后端(locmem/fakeredis) @@ -290,26 +309,27 @@ markers = --- -## 十一、CI 自动化运行 +## 十二、CI 自动化运行 -### 11.1 触发策略 +### 12.1 触发策略 - 每日定时全量测试 - `main/develop` 每次 push 触发 -### 11.2 流水线拆分 +### 12.2 流水线拆分 1. `unit-and-integration` 2. `e2e`(依赖前者成功后执行) -### 11.3 最低产物 +### 12.3 最低产物 - 覆盖率报告(终端 + 平台上传) - E2E 失败截图 artifact +- 测试结果明细(至少含 `run_id`、`test_case_id`、`step_id`、`status`、`error_message`、`expected_result`、`actual_result`) --- -## 十二、AI 协作测试要求 +## 十三、AI 协作测试要求 每个 User Story 实现后,必须同时补齐: @@ -326,7 +346,7 @@ markers = --- -## 十三、禁止项(Do NOT) +## 十四、禁止项(Do NOT) - 禁止 Django 原生 `Client()` 进行租户集成测试 - 禁止固定等待(`sleep` / `wait_for_timeout`) @@ -334,12 +354,14 @@ markers = - 禁止测试之间共享可变数据 - 禁止无权限/未登录场景缺失 - 禁止空测试占位后不补全 +- 禁止未分配 `TC-FON-XXXXXX` 的匿名测试入库 --- -## 十四、文档同步规则 +## 十五、文档同步规则 - 新增/调整 User Story:同步 `PRD/TASK.md` 与集成测试映射 - 模块 API 变更:同步对应模块技术方案 - 测试目录变更:同步本文件目录结构与 CI 脚本 - 新增测试基建(fixture/工具):同步 `AGENTS.md` 与本文件 +- 新增测试用例:同步 `TEST_CASES/TEST_CASE_REGISTRY.md`(编号段、水位、状态) diff --git a/Project/fonrey/TECH_STACK/登录管理技术方案.md b/Project/fonrey/TECH_STACK/登录管理技术方案.md index c038d72d..1f14f342 100644 --- a/Project/fonrey/TECH_STACK/登录管理技术方案.md +++ b/Project/fonrey/TECH_STACK/登录管理技术方案.md @@ -2,281 +2,269 @@ # Fonrey 登录管理技术方案 -**版本**: 3.1 +**版本**: 4.0 **项目**: Fonrey 房产经纪管理系统 **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery -**关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md` +**关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md`(v2.0) **关联数据模型**: `DATA_MODEL/DATA_MODEL_LOGIN.md`(本方案不重复 DDL) **关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威) -**最后更新**: 2026-04-27 +**关联测试规范**: `TECH_STACK/测试规范.md`、`TEST_CASES/TEST_CASES_LOGIN_MODULE.md` +**最后更新**: 2026-04-30 --- ## 一、文档定位与边界 -本文件仅定义登录模块的: +本文件定义登录模块的实现口径: -1. 模块边界与服务职责 -2. API 端点设计(页面 / HTMX / JSON) -3. 登录安全策略(验证码、锁定、会话、找回) -4. 缓存与异步任务策略 +1. 模块范围与职责边界 +2. API 端点(页面 / HTMX / JSON) +3. 安全策略(滑块验证、登录锁定、短信 OTP、会话) +4. Redis/Celery 运行策略 5. 错误码与测试映射 -> 不在本文件展开表字段、索引、DDL。数据结构以 `DATA_MODEL_LOGIN.md` 为唯一权威。 +> 本文件不展开数据表字段与索引。数据结构以 `DATA_MODEL_LOGIN.md` 为唯一权威。 --- -## 二、范围定义(以 P0 为准) +## 二、范围定义(以 PRD v2.0 为准) ### 2.1 P0 必须覆盖 -- Tenant ID 校验(Public Schema) -- 用户名 + 密码登录(Tenant Schema) -- 验证码挑战与一次性 pass token -- 连续失败锁定与解锁机制 -- 找回用户名 / 重置密码 -- 首次登录强制改密 -- 安全登出与会话销毁 +- Tenant Code 识别(首次启动 + 切换公司) +- 密码登录(手机号/密码 + 滑块) +- 首次登录强制修改密码 +- 找回密码(纯短信三步流程) +- 手机验证码登录(MVP 正式功能) +- 登录失败锁定 / 自动解锁 / 管理员解锁 +- 安全登出与会话失效 -### 2.2 预留(非本期强制) +### 2.2 非目标 / 预留 -- MFA(OTP / 短信) +- 微信扫码登录(仅保留禁用入口与接口占位,不开放功能) - 企业 SSO(OAuth2 / SAML) -- 设备指纹与风险评分 +- 风险评分/设备指纹 + +### 2.3 已废弃(不得实现) + +- 找回用户名流程(Story 4 已废弃) --- ## 三、模块架构边界 -## 3.1 模块职责(`apps/account`) +### 3.1 模块职责(`apps/account`) -- 登录认证入口与 Session 建立 -- 密码策略、锁定策略、登录防刷策略执行 -- 找回流程与一次性重置令牌管理 -- 登录/登出/失败审计事件写入 +- 租户识别与租户上下文建立前置校验 +- 登录鉴权、会话签发、登出销毁 +- 首登改密门禁(`is_initial_password`) +- 短信 OTP 发送/校验(找回密码 + 验证码登录) +- 登录与安全审计事件写入 -## 3.2 多租户分层职责 +### 3.2 多租户分层职责 | 层级 | Schema | 职责 | |---|---|---| -| Tenant ID 校验 | Public | 校验租户标识、域名映射、租户状态 | -| 登录认证 | Tenant | 账号鉴权、失败计数、会话建立 | -| 密码找回 | Tenant | 身份确认、令牌签发与核销 | +| Tenant Code 校验 | Public | 校验 `tenant_code`、租户状态、品牌信息返回 | +| 登录认证 | Tenant | 账号鉴权、失败计数、锁定状态、会话签发 | +| 短信 OTP | Tenant | OTP 记录、场景区分、过期与尝试次数控制 | -## 3.3 外部依赖 +### 3.3 外部依赖 | 依赖模块 | 用途 | |---|---| -| `apps/org` | 员工状态联动(离职/冻结禁止登录) | -| `core/encryption.py` | 手机号等敏感信息加密与脱敏 | -| `core/cache.py` | 验证码票据、失败计数、频控缓存 | -| `Celery` | 找回消息异步发送、安全日报任务 | +| `apps/org` | 员工状态联动(离职/停用不可登录) | +| `core/encryption.py` | 手机号加密/哈希能力 | +| `core/cache.py` | 滑块票据、频控、锁定计数、重置 token | +| `Celery` | 短信发送异步化(可选,建议) | --- ## 四、API 设计原则 -1. 登录链路固定三段:Tenant 校验 → 验证码 → 账号认证。 -2. 最小暴露原则:账号不存在与密码错误统一返回。 -3. 认证状态以数据库为准,Redis 仅做加速与频控。 -4. 安全相关写操作后立即失效缓存,不依赖 TTL。 -5. HTMX 返回片段,JSON 返回结构化错误体。 +1. 登录安全链路:**Tenant 校验 → 滑块验证 → 登录提交**。 +2. 防枚举:账号不存在/停用等敏感状态采用统一外显文案。 +3. 账号锁定是账号维度策略,不区分密码登录或验证码登录。 +4. Redis 仅作运行态与频控,最终状态以数据库持久化字段为准。 +5. 所有登录相关接口遵循 `TECH_STACK/API_CONTRACT.md` 的统一错误响应格式。 --- -## 五、端点清单(核心) +## 五、端点清单(对齐 PRD) -## 5.1 页面路由(SSR) +### 5.1 页面路由(SSR) | 路径 | 方法 | 鉴权 | 说明 | |---|---|---|---| -| `/account/tenant/verify/` | GET | 否 | 首次租户识别页 | -| `/account/login/` | GET | 否 | 登录页 | -| `/account/change-password/` | GET | 是 | 首登强制改密页 | -| `/account/recover-username/` | GET | 否 | 找回用户名页 | -| `/account/recover-password/` | GET | 否 | 找回密码页 | +| `/auth/tenant/identify/` | GET | 否 | Tenant 识别页 | +| `/auth/login/` | GET | 否 | 登录页(密码登录/验证码登录 Tab) | +| `/auth/password/forgot/` | GET | 否 | 找回密码三步页 | +| `/auth/password/change-initial/` | GET | 是 | 首次登录强制改密页 | -## 5.2 HTMX 片段端点 +> 说明:`/auth/wechat/*` 仅预留,不在 MVP 开放。 + +### 5.2 HTMX 片段端点 | 路径 | 方法 | 用途 | 返回 | |---|---|---|---| -| `/account/fragments/login-form/` | GET | 登录表单局刷 | HTML 片段 | -| `/account/fragments/captcha/` | GET | 验证码区块刷新 | HTML 片段 | -| `/account/fragments/recover-step/` | GET | 找回步骤局刷 | HTML 片段 | +| `/auth/fragments/captcha/` | GET | 刷新滑块区块 | HTML 片段 | +| `/auth/fragments/login-form/` | GET | 登录 Tab 内容局刷 | HTML 片段 | +| `/auth/fragments/forgot-step/` | GET | 找回密码步骤局刷 | HTML 片段 | -## 5.3 JSON API(P0) +### 5.3 JSON API(MVP) | 端点 | 方法 | 说明 | |---|---|---| -| `/api/account/tenant/verify/` | POST | 校验 tenant_id 与可用性 | -| `/api/account/captcha/generate/` | POST | 生成验证码 challenge | -| `/api/account/captcha/verify/` | POST | 校验 challenge 并签发 pass token | -| `/api/account/login/` | POST | 登录并建立 session | -| `/api/account/logout/` | POST | 登出并销毁 session | -| `/api/account/password/change/` | POST | 登录态改密 | -| `/api/account/username/recover/` | POST | 找回用户名 | -| `/api/account/password/recover/request/` | POST | 申请重置密码令牌 | -| `/api/account/password/recover/reset/` | POST | 使用令牌重置密码 | +| `/api/auth/tenant/verify/` | POST | Tenant Code 校验(公开接口) | +| `/api/auth/captcha/` | GET | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token) | +| `/api/auth/captcha/verify/` | POST | 校验滑块并签发 `captcha_pass_token` | +| `/api/auth/login/` | POST | 密码登录 | +| `/api/auth/login/phone/` | POST | 手机验证码登录 | +| `/api/auth/recover/password/request/` | POST | 找回密码步骤一:发 OTP | +| `/api/auth/recover/password/verify/` | POST | 找回密码步骤二:校验 OTP,颁发 `sms_reset_token` | +| `/api/auth/recover/password/reset/` | POST | 找回密码步骤三:提交新密码 | +| `/api/auth/password/change-initial/` | POST | 首次登录强制改密提交 | +| `/api/auth/logout/` | POST | 登出销毁会话 | --- -## 六、关键 API 规范(请求/响应) +## 六、关键流程约束 -## 6.1 登录 +### 6.1 Tenant 识别 -`POST /api/account/login/` +- `tenant_code` 固定 12 位数字,前后空格自动 trim +- 成功返回:租户名称、Logo URL、登录地址 +- 失败返回:`valid=false` + 统一错误信息 +- 接口公开但必须限流:单 IP 每分钟 ≤ 10 次 + +### 6.2 密码登录 + +请求体(示例): ```json { - "tenant_id": "fonrey-sh", - "username": "agent_001", - "password": "******", + "phone": "13800138000", + "password": "***", "captcha_pass_token": "token" } ``` -成功 `200`: +关键规则: +- 滑块通过后方可提交登录 +- 密码连续错误 ≥ 5 次,锁定 30 分钟 +- `is_initial_password = true` 时强制跳转改密页 +- 错误文案统一,不泄露账号存在性细节 + +### 6.3 找回密码(纯短信三步) + +#### 步骤一:发送 OTP +- `scene = password_reset` +- OTP 有效期:10 分钟 +- 同手机号频控:5 次/小时 +- 手机号不存在/停用:统一提示“如该手机号已注册,验证码将在 1 分钟内发送” + +#### 步骤二:校验 OTP +- 正确且未过期:签发 `sms_reset_token`(15 分钟,一次性) +- 错误:累计尝试,≥5 次作废该 OTP +- 过期:提示重新获取 + +#### 步骤三:重置密码 +- 必须携带有效 `sms_reset_token` +- 成功后:`is_initial_password = false` +- 该用户所有会话立即失效,跳回登录页 + +### 6.4 手机验证码登录(MVP 正式) + +请求体(示例): ```json { - "message": "登录成功", - "redirect_url": "/home/" + "phone": "13800138000", + "sms_code": "123456" } ``` -失败码:`ACCOUNT_LOGIN_INVALID_CREDENTIAL` / `ACCOUNT_LOCKED` / `ACCOUNT_CAPTCHA_INVALID` - -## 6.2 申请重置密码 - -`POST /api/account/password/recover/request/` - -```json -{ - "tenant_id": "fonrey-sh", - "username": "agent_001", - "contact": "138****0000" -} -``` - -规则: -- 频率限制(建议 5 次/小时) -- 统一返回文案,避免枚举账号存在性 +关键规则: +- 获取登录验证码前必须先通过滑块 +- `scene = login` +- OTP 有效期:5 分钟 +- 同手机号频控:10 次/小时(与 `password_reset` 独立计数) +- OTP 错误 ≥5 次作废 +- 账号 `locked` 或 `disabled` 时,验证码登录同样拒绝 --- -## 七、HTMX 交互约定 - -## 7.1 Header 约定 - -- 请求头:`HX-Request: true` -- 成功触发:`HX-Trigger: {"toast":{"level":"success","message":"操作成功"}}` -- 失败触发:`HX-Trigger: {"toast":{"level":"error","message":"操作失败"}}` -- 登录成功:`HX-Redirect: /home/` - -## 7.2 模板分片命名 - -- `templates/account/fragments/login_form.html` -- `templates/account/fragments/captcha_panel.html` -- `templates/account/fragments/recover_step.html` - ---- - -## 八、权限与数据范围 - -## 8.1 访问控制 - -- 匿名可访问:租户校验、登录、找回相关端点 -- 登录后访问:改密、登出 -- 首登未改密用户仅允许访问改密页面 - -## 8.2 审计字段要求 - -登录与找回相关操作至少记录: -- tenant_schema -- username(或脱敏标识) -- ip / user_agent -- result_code -- created_at - ---- - -## 九、异步任务与缓存策略 - -## 9.1 Celery 任务 - -| 任务 | 触发时机 | 说明 | -|---|---|---| -| `account_send_recover_message_task` | 找回请求成功后 | 异步发送邮件/短信 | -| `account_security_digest_task` | 定时任务 | 汇总锁定、失败、异常登录统计 | - -## 9.2 Redis Key 规范 +## 七、Redis Key 规范(对齐 PRD) | Key | TTL | 说明 | |---|---|---| -| `{schema}:account:captcha:{challenge_id}` | 180s | 验证码挑战态 | -| `{schema}:account:captcha:pass:{token}` | 180s | 验证通过一次性票据 | -| `{schema}:account:login_fail:{username}` | 1800s | 登录失败计数 | -| `{schema}:account:recover:rate:{username}` | 3600s | 找回频控 | +| `captcha_token:{uuid}` | 3 分钟 | 滑块验证会话 | +| `captcha_pass:{uuid}` | 3 分钟 | 一次性通过凭证 | +| `login_fail:{tenant_id}:{username}` | 30 分钟 | 登录失败计数 | +| `sms_limit:password_reset:{phone}` | 1 小时 | 找回密码 OTP 发送频控(≤5次) | +| `sms_limit:login:{phone}` | 1 小时 | 登录 OTP 发送频控(≤10次) | +| `sms_reset_token:{token}` | 15 分钟 | 找回密码步骤三凭证 | +| `tenant_verify_ip:{ip}` | 1 分钟 | Tenant 校验接口 IP 限流(≤10次) | --- -## 十、性能与可靠性约束 +## 八、安全与合规 -- 登录链路接口目标:`p95 < 300ms`(不含外部消息发送) -- 找回请求接口目标:`p95 < 400ms`(消息发送异步化) -- 缓存故障时系统应降级到 DB 校验,但保留频控硬兜底 -- 验证码组件不可用时,应返回明确错误并禁止跳过登录防护 +1. 密码仅允许 Django 安全哈希(PBKDF2/Argon2)。 +2. OTP 明文不得入库,仅存哈希。 +3. 敏感字段日志脱敏(手机号、token、验证码)。 +4. 会话过期或登出后,受保护页面必须重定向登录。 +5. HTTPS 强制,不允许明文传输降级。 --- -## 十一、安全与合规 +## 九、错误码建议(登录模块) -1. 密码仅允许 Django 安全哈希(PBKDF2 / Argon2)。 -2. 连续失败 N 次锁定(建议 5 次,30 分钟)。 -3. 重置令牌一次性使用,过期失效,不可复用。 -4. 登出必须销毁会话,旧页面刷新需重定向登录。 -5. 敏感字段仅脱敏返回,禁止明文日志输出。 - ---- - -## 十二、错误码建议 - -| code | HTTP | 场景 | +| code | HTTP | 中文含义 | |---|---|---| -| `ACCOUNT_TENANT_INVALID` | 400 | tenant 无效或停用 | -| `ACCOUNT_CAPTCHA_INVALID` | 400 | 验证码失败/过期 | -| `ACCOUNT_LOGIN_INVALID_CREDENTIAL` | 401 | 账号或密码错误 | -| `ACCOUNT_LOCKED` | 423 | 账号锁定中 | -| `ACCOUNT_PASSWORD_WEAK` | 400 | 新密码不满足复杂度 | -| `ACCOUNT_RESET_TOKEN_INVALID` | 400 | 重置令牌无效/已使用 | +| `AUTH_TENANT_NOT_FOUND` | 400 | 租户识别码无效 | +| `AUTH_TENANT_RATE_LIMITED` | 429 | Tenant 校验请求过于频繁 | +| `AUTH_CAPTCHA_INVALID` | 400 | 滑块验证失败 | +| `AUTH_INVALID_CREDENTIAL` | 401 | 手机号或密码错误 | +| `AUTH_ACCOUNT_LOCKED` | 423 | 账号已锁定 | +| `AUTH_ACCOUNT_DISABLED` | 403 | 账号已停用 | +| `AUTH_SMS_OTP_INVALID` | 400 | 短信验证码错误 | +| `AUTH_SMS_OTP_EXPIRED` | 400 | 短信验证码过期 | +| `AUTH_SMS_OTP_RATE_LIMITED` | 429 | OTP 发送超限 | +| `AUTH_SMS_RESET_TOKEN_INVALID` | 400 | 重置凭证无效或过期 | +| `AUTH_PASSWORD_WEAK` | 400 | 新密码不满足强度规则 | --- -## 十三、测试映射(P0) +## 十、测试映射(与全局编号对齐) -| 场景 | 最低覆盖 | -|---|---| -| Tenant 校验 | 有效/无效/停用租户 | -| 登录链路 | 验证码通过、失败锁定、解锁后登录 | -| 找回流程 | 频控、令牌一次性、过期处理 | -| 首登改密 | 未改密不可进入业务页 | -| 登出 | 会话销毁、回退重定向 | +- 测试编号规范:`TEST_CASES/TEST_CASE_ID_SPEC.md` +- 注册表:`TEST_CASES/TEST_CASE_REGISTRY.md` +- 登录模块用例:`TEST_CASES/TEST_CASES_LOGIN_MODULE.md` -测试文件:`tests/integration/account/test_us_account.py` +建议最小执行集: +- Tenant 识别:`TC-FON-000001` ~ `000010` +- 密码登录与锁定:`TC-FON-000011` ~ `000028` +- 首登改密:`TC-FON-000029` ~ `000033` +- 找回密码:`TC-FON-000034` ~ `000044` +- 验证码登录:`TC-FON-000045` ~ `000048` --- -## 十四、落地顺序建议 +## 十一、落地顺序建议 -1. 先实现 tenant 校验 + 登录主链路 -2. 再接验证码 + 失败锁定 -3. 再实现找回用户名/密码 -4. 最后补安全摘要任务与审计报表 +1. Tenant 识别 + 密码登录主链路 +2. 滑块与失败锁定 +3. 首登改密门禁 +4. 找回密码三步(短信) +5. 手机验证码登录(scene=login) +6. 报表与监控补齐 --- -## 十五、文档同步规则 +## 十二、文档同步规则 -- 登录数据结构调整:同步 `DATA_MODEL_LOGIN.md` -- 安全策略或门禁调整:同步 `权限管理系统技术方案.md` -- API 变更:同步本文件与登录 PRD 验收条目 +- PRD 登录模块变更:同步本文件 +- 数据结构调整:同步 `DATA_MODEL_LOGIN.md` +- 测试用例新增/变更:同步 `TEST_CASES/TEST_CASE_REGISTRY.md` 与登录用例文档 +- API 契约调整:同步 `TECH_STACK/API_CONTRACT.md` diff --git a/Project/fonrey/TEST_CASES/TEST_CASES_LOGIN_MODULE.md b/Project/fonrey/TEST_CASES/TEST_CASES_LOGIN_MODULE.md new file mode 100644 index 00000000..228eccd7 --- /dev/null +++ b/Project/fonrey/TEST_CASES/TEST_CASES_LOGIN_MODULE.md @@ -0,0 +1,390 @@ +# Fonrey 登录模块测试用例文档(可自动化) + +> 文档版本:v1.0 +> 适用范围:`PRD/登录管理/用户登录管理模块PRD.md` v2.0 +> 用例编号范围:`TC-FON-000001` ~ `TC-FON-000048`(全局唯一) +> 编号规范:`TEST_CASES/TEST_CASE_ID_SPEC.md` + +--- + +## 1. 目标与原则 + +1. 本文档用于让工程师直接生成自动化测试代码(API 集成测试 + Web E2E)。 +2. 每个测试用例均包含**唯一ID**和**步骤ID**,可直接用于失败定位与测试报告。 +3. 用例覆盖登录模块 MVP 正式范围: + - Story 1:Tenant 识别 + - Story 2:手机号+密码登录 + - Story 3:短信找回密码 + - Story 5:短信验证码登录 +4. Story 4(找回用户名)已废弃,不实现。 +5. Story 6(微信扫码)为预留,不实现,仅验证禁用态。 + +--- + +## 2. 自动化执行与报告要求(工程实现必须遵循) + +- 执行层: + - API 集成:`pytest + pytest-django + TenantClient` + - Web E2E:`playwright` +- 每步必须输出:`run_id / test_case_id / step_id / status / expected_result / actual_result / error_message` +- 失败时必须附带: + - Web:截图路径 + - API:请求响应快照 + 关键日志路径 + +--- + +## 3. 全量测试用例清单(48条) + +### 3.0 登录模块 API 端点口径(以 PRD 为准) + +- `POST /api/auth/tenant/verify/` +- `GET /api/auth/captcha/` +- `POST /api/auth/captcha/verify/` +- `POST /api/auth/login/` +- `POST /api/auth/login/phone/` +- `POST /api/auth/recover/password/request/` +- `POST /api/auth/recover/password/verify/` +- `POST /api/auth/recover/password/reset/` +- `POST /api/auth/logout/` + +> 注:测试代码生成与接口调用必须使用以上路径。 + +## A. Tenant 识别(Story 1) + +### TC-FON-000001 Tenant Code 页面首启展示 +- 级别:E2E +- 前置:本地无 Tenant Code 缓存 +- 步骤: + - `TC-FON-000001-S01` 启动应用/打开登录入口页 + - `TC-FON-000001-S02` 检查是否进入 Tenant 识别页 + - `TC-FON-000001-S03` 校验页面元素(Logo、文案、输入框、确认按钮) +- 预期:显示 Tenant 识别页且元素完整 + +### TC-FON-000002 Tenant Code 输入去空格与粘贴 +- 级别:E2E +- 前置:在 Tenant 识别页 +- 步骤: + - `TC-FON-000002-S01` 粘贴 ` 202500010001 ` + - `TC-FON-000002-S02` 点击确认 + - `TC-FON-000002-S03` 观察发送请求参数 +- 预期:请求中的 tenant_code 为 `202500010001` + +### TC-FON-000003 Tenant Code 非12位拦截 +- 级别:E2E +- 前置:Tenant 识别页 +- 步骤: + - `TC-FON-000003-S01` 输入 `20250001` + - `TC-FON-000003-S02` 点击确认 + - `TC-FON-000003-S03` 检查错误提示 +- 预期:提示“识别码须为12位数字”,不发请求 + +### TC-FON-000004 Tenant 验证成功流程 +- 级别:API+E2E +- 前置:合法 tenant_code 存在 +- 步骤: + - `TC-FON-000004-S01` 调用 `POST /api/auth/tenant/verify/` + - `TC-FON-000004-S02` 校验 `valid=true` 且返回租户品牌信息 + - `TC-FON-000004-S03` 前端跳转登录页并展示“正在登录:XX房产” +- 预期:成功跳转并写入本地缓存 + +### TC-FON-000005 Tenant 验证失败(无效识别码) +- 级别:API+E2E +- 前置:tenant_code 不存在 +- 步骤: + - `TC-FON-000005-S01` 调用 `POST /api/auth/tenant/verify/` + - `TC-FON-000005-S02` 校验 `valid=false` 与错误码 + - `TC-FON-000005-S03` 校验前端错误提示与不落缓存 +- 预期:提示“识别码无效...”,允许重试 + +### TC-FON-000006 Tenant 验证网络异常重试 +- 级别:E2E +- 前置:模拟网络失败 +- 步骤: + - `TC-FON-000006-S01` 点击确认触发请求失败 + - `TC-FON-000006-S02` 检查“网络连接失败”提示 + - `TC-FON-000006-S03` 点击重试并恢复 +- 预期:重试可再次发起请求 + +### TC-FON-000007 非首启跳过识别页 +- 级别:E2E +- 前置:本地已有合法 Tenant Code +- 步骤: + - `TC-FON-000007-S01` 打开应用 + - `TC-FON-000007-S02` 检查是否直接进入登录页 + - `TC-FON-000007-S03` 检查租户品牌信息回填 +- 预期:跳过识别页 + +### TC-FON-000008 切换公司入口流程 +- 级别:E2E +- 前置:已在登录页 +- 步骤: + - `TC-FON-000008-S01` 点击“切换公司” + - `TC-FON-000008-S02` 校验二次确认文案 + - `TC-FON-000008-S03` 确认后检查缓存清除并回到识别页 +- 预期:缓存清除成功并跳回识别页 + +### TC-FON-000009 Tenant 验证接口无需鉴权 +- 级别:API +- 前置:未登录 +- 步骤: + - `TC-FON-000009-S01` 不带 token 调用验证接口 + - `TC-FON-000009-S02` 校验响应状态 + - `TC-FON-000009-S03` 校验业务结构 +- 预期:接口可访问(非401/403) + +### TC-FON-000010 Tenant 验证接口IP限流 +- 级别:API +- 前置:同IP连续请求 +- 步骤: + - `TC-FON-000010-S01` 1分钟内发起11次请求 + - `TC-FON-000010-S02` 校验前10次正常 + - `TC-FON-000010-S03` 校验第11次被限流 +- 预期:触发每分钟≤10次限制 + +--- + +## B. 密码登录(Story 2) + +### TC-FON-000011 密码登录页元素完整 +- 级别:E2E +- 步骤:`S01` 打开密码登录页;`S02` 检查手机号/密码/滑块/登录按钮;`S03` 检查忘记密码入口 +- 预期:元素齐全 + +### TC-FON-000012 手机号输入仅数字11位 +- 级别:E2E +- 步骤:`S01` 输入含字母字符;`S02` 观察自动过滤;`S03` 检查长度限制 +- 预期:仅数字,最长11位 + +### TC-FON-000013 密码明文/密文切换 +- 级别:E2E +- 步骤:`S01` 输入密码;`S02` 点击眼睛图标显示明文;`S03` 再次点击恢复密文 +- 预期:切换正常 + +### TC-FON-000014 三项未完成时登录按钮置灰 +- 级别:E2E +- 步骤:`S01` 只填手机号;`S02` 再填密码;`S03` 未完成滑块时检查按钮状态 +- 预期:按钮不可点击 + +### TC-FON-000015 滑块验证失败不计入密码错误次数 +- 级别:API+E2E +- 步骤: + - `TC-FON-000015-S01` 调用 `GET /api/auth/captcha/` 获取 challenge + - `TC-FON-000015-S02` 调用 `POST /api/auth/captcha/verify/` 提交错误轨迹 + - `TC-FON-000015-S03` 检查滑块失败提示与刷新,且登录失败计数未增加 +- 预期:不计入账号锁定计数 + +### TC-FON-000016 滑块验证成功状态保持至提交 +- 级别:E2E +- 步骤:`S01` 完成滑块;`S02` 检查“验证通过”状态;`S03` 立即点登录 +- 预期:状态有效并允许提交 + +### TC-FON-000017 登录前端校验-手机号为空 +- 级别:E2E +- 步骤:`S01` 留空手机号;`S02` 点登录;`S03` 检查提示 +- 预期:提示“请输入手机号” + +### TC-FON-000018 登录前端校验-手机号不足11位 +- 级别:E2E +- 步骤:`S01` 输入10位;`S02` 点登录;`S03` 检查提示 +- 预期:提示“请输入完整的11位手机号” + +### TC-FON-000019 登录前端校验-密码为空 +- 级别:E2E +- 步骤:`S01` 填手机号与滑块通过;`S02` 密码留空;`S03` 点登录 +- 预期:提示“请输入密码” + +### TC-FON-000020 登录前端校验-未完成滑块 +- 级别:E2E +- 步骤:`S01` 填手机号密码;`S02` 不做滑块;`S03` 点登录 +- 预期:提示“请完成滑块验证” + +### TC-FON-000021 密码登录成功(is_initial_password=False) +- 级别:API+E2E +- 步骤: + - `TC-FON-000021-S01` 调用 `POST /api/auth/login/` 提交正确手机号/密码与 `captcha_pass_token` + - `TC-FON-000021-S02` 校验返回 token 与 `is_initial_password=false` + - `TC-FON-000021-S03` 校验跳首页欢迎语 +- 预期:登录成功进入首页 + +### TC-FON-000022 密码登录成功(is_initial_password=True) +- 级别:API+E2E +- 步骤:`S01` 使用初始密码登录;`S02` 校验返回标志true;`S03` 校验跳转强制改密页 +- 预期:不可访问其他页面 + +### TC-FON-000023 密码错误提示统一 +- 级别:API+E2E +- 步骤:`S01` 提交错误密码;`S02` 校验错误文案;`S03` 校验密码框清空+手机号保留+滑块刷新 +- 预期:提示“手机号或密码错误,请重新输入” + +### TC-FON-000024 连续错误5次触发账号锁定 +- 级别:API +- 步骤:`S01` 连续5次密码错误;`S02` 校验第5次后状态locked;`S03` 校验locked_until=now+30min +- 预期:账号锁定成功 + +### TC-FON-000025 锁定期间登录被拒绝 +- 级别:API+E2E +- 步骤:`S01` 对locked账号发起登录;`S02` 校验提示文案;`S03` 校验登录按钮置灰 +- 预期:拒绝登录 + +### TC-FON-000026 30分钟后自动解锁 +- 级别:API +- 步骤:`S01` 构造锁定到期账号;`S02` 时间推进30分钟后重试;`S03` 校验恢复登录能力 +- 预期:自动解锁 + +### TC-FON-000027 账号停用登录拦截 +- 级别:API+E2E +- 步骤:`S01` 停用账号发起登录;`S02` 校验错误码;`S03` 校验前端提示 +- 预期:提示“账号已停用,请联系您的管理员” + +### TC-FON-000028 Session过期跳转登录 +- 级别:E2E +- 步骤:`S01` 进入受保护页面;`S02` 使session过期;`S03` 执行动作触发跳转 +- 预期:跳回登录并提示“登录已过期,请重新登录” + +--- + +## C. 首次登录强制改密(Story 2/3 公共密码组件) + +### TC-FON-000029 首次登录强制改密页不可跳过 +- 级别:E2E +- 步骤:`S01` 初始密码登录;`S02` 尝试访问其他功能路由;`S03` 检查仍停留改密流程 +- 预期:不可绕过 + +### TC-FON-000030 新密码强度校验 +- 级别:API+E2E +- 步骤:`S01` 输入弱密码;`S02` 提交;`S03` 检查强度错误提示 +- 预期:拒绝弱密码 + +### TC-FON-000031 新密码不得与最近3次重复 +- 级别:API +- 步骤:`S01` 构造历史密码3条;`S02` 提交重复密码;`S03` 校验错误码 +- 预期:拒绝历史重复 + +### TC-FON-000032 首次改密成功后状态更新 +- 级别:API +- 步骤:`S01` 提交合法新密码;`S02` 校验is_initial_password=false;`S03` 校验password_histories新增 +- 预期:状态正确更新 + +### TC-FON-000033 首次改密成功后直接进入系统 +- 级别:E2E +- 步骤:`S01` 完成改密提交;`S02` 检查成功反馈;`S03` 校验跳首页 +- 预期:进入首页 + +--- + +## D. 找回密码(Story 3) + +### TC-FON-000034 忘记密码入口可达 +- 级别:E2E +- 步骤:`S01` 登录页点击忘记密码;`S02` 校验进入Stepper;`S03` 校验步骤一元素 +- 预期:进入找回密码流程 + +### TC-FON-000035 步骤一手机号格式校验 +- 级别:E2E +- 步骤:`S01` 输入不足11位手机号;`S02` 点获取验证码;`S03` 检查提示 +- 预期:提示“请输入完整的11位手机号” + +### TC-FON-000036 步骤一发送验证码成功(active账号) +- 级别:API+E2E +- 步骤: + - `TC-FON-000036-S01` 调用 `POST /api/auth/recover/password/request/` 提交 active 手机号 + - `TC-FON-000036-S02` 校验进入 60 秒倒计时 + - `TC-FON-000036-S03` 校验 `sms_otp_records` 写入 `scene=password_reset` 且有效期 10 分钟 +- 预期:发送成功并记录正确 + +### TC-FON-000037 步骤一防枚举响应(不存在/停用账号) +- 级别:API +- 步骤:`S01` 分别提交不存在和停用手机号;`S02` 校验响应文案一致;`S03` 校验不泄露账号状态 +- 预期:统一提示“如该手机号已注册...” + +### TC-FON-000038 找回密码短信发送频控(5次/小时) +- 级别:API +- 步骤:`S01` 同手机号发送6次;`S02` 校验前5次可用;`S03` 校验第6次超限 +- 预期:提示“发送次数过多,请1小时后再试” + +### TC-FON-000039 步骤二验证码正确进入步骤三 +- 级别:API+E2E +- 步骤: + - `TC-FON-000039-S01` 调用 `POST /api/auth/recover/password/verify/` 提交正确 6 位验证码 + - `TC-FON-000039-S02` 校验颁发 `sms_reset_token`(15 分钟) + - `TC-FON-000039-S03` 页面进入步骤三 +- 预期:通过校验并进入重置页 + +### TC-FON-000040 步骤二验证码错误与5次作废 +- 级别:API +- 步骤:`S01` 连续输入错误验证码5次;`S02` 检查前4次提示错误;`S03` 第5次后验证码作废 +- 预期:提示“验证码已失效,请重新获取” + +### TC-FON-000041 步骤二验证码过期 +- 级别:API +- 步骤:`S01` 使用超时验证码提交;`S02` 校验错误码;`S03` 校验提示文案 +- 预期:提示“验证码已过期,请重新获取” + +### TC-FON-000042 步骤三token无效或过期 +- 级别:API+E2E +- 步骤:`S01` 使用无效sms_reset_token访问步骤三;`S02` 校验错误提示;`S03` 跳回步骤一 +- 预期:提示“操作已超时,请重新发起找回密码” + +### TC-FON-000043 步骤三重置成功后会话失效 +- 级别:API +- 步骤: + - `TC-FON-000043-S01` 调用 `POST /api/auth/recover/password/reset/`(携带有效 `sms_reset_token`)重置密码 + - `TC-FON-000043-S02` 校验 `is_initial_password=false` + - `TC-FON-000043-S03` 校验该用户历史 session 失效 +- 预期:需重新登录 + +### TC-FON-000044 重置成功后用新密码登录 +- 级别:E2E +- 步骤:`S01` 完成找回密码全流程;`S02` 跳回登录页;`S03` 用新密码登录成功 +- 预期:登录成功,提示“密码已重置,请使用新密码登录” + +--- + +## E. 验证码登录(Story 5) + +### TC-FON-000045 登录方式Tab切换与状态重置 +- 级别:E2E +- 步骤:`S01` 从密码登录切换到验证码登录;`S02` 检查表单区切换;`S03` 校验原输入与滑块状态清空 +- 预期:状态重置正确 + +### TC-FON-000046 未完成滑块禁止获取登录验证码 +- 级别:E2E +- 步骤:`S01` 进入验证码登录Tab;`S02` 直接点获取验证码;`S03` 检查提示 +- 预期:提示“请先完成滑块验证” + +### TC-FON-000047 登录验证码发送与频控(10次/小时,独立于找回密码) +- 级别:API +- 步骤:`S01` 发送登录验证码11次(scene=login);`S02` 校验第11次超限;`S03` 校验找回密码scene不受影响 +- 预期:login场景10次/小时,scene隔离计数 + +### TC-FON-000048 验证码登录成功/失败/锁定限制 +- 级别:API+E2E +- 步骤: + - `TC-FON-000048-S01` 调用 `POST /api/auth/login/phone/`,正确 OTP 登录成功,返回 token 与 `is_initial_password` + - `TC-FON-000048-S02` 调用 `POST /api/auth/login/phone/`,错误 OTP 提示“验证码有误”,5 次后作废 + - `TC-FON-000048-S03` 账号 locked 时调用 `POST /api/auth/login/phone/` 同样被拒绝 +- 预期:三类行为均符合 PRD + +--- + +## 4. 工程实现指引(给测试开发工程师) + +1. **目录建议** + - `tests/integration/login/test_tc_fon_000001_000048.py` + - `tests/e2e/login/test_tc_fon_000001_000048.spec.ts` +2. **命名规范** + - 函数名必须带用例ID,例如:`def test_tc_fon_000024_account_lock_after_5_failures():` +3. **步骤日志** + - 每步执行前后打印 step_id;断言失败时把 step_id 写入异常消息 +4. **报告聚合** + - 生成 `reports/login_run_.json` + `reports/login_run_.html` +5. **CI 门禁** + - `TC-FON-000001` ~ `TC-FON-000048` 全量通过才允许合并 + +--- + +## 5. 变更规则 + +- 新增登录用例:从 `TC-FON-000049` 开始递增 +- 后续房源/客源模块:继续用同一全局序列,不得重号 +- 禁止删除历史用例ID;可标记 `deprecated` 但保留编号与历史报告可追溯性 diff --git a/Project/fonrey/TEST_CASES/TEST_CASE_ID_SPEC.md b/Project/fonrey/TEST_CASES/TEST_CASE_ID_SPEC.md new file mode 100644 index 00000000..bf2f33db --- /dev/null +++ b/Project/fonrey/TEST_CASES/TEST_CASE_ID_SPEC.md @@ -0,0 +1,56 @@ +# 测试用例全局编号规范(Fonrey) + +## 1. 目标 +确保所有模块(登录、房源、客源等)测试用例编号**全局唯一**,便于自动化执行、失败定位、统计报表。 + +## 2. 编号规则 + +- **测试用例ID**:`TC-FON-XXXXXX` + - `TC`:Test Case + - `FON`:Fonrey + - `XXXXXX`:6位递增数字,左侧补0(如 `000001`) +- **步骤ID**:`TC-FON-XXXXXX-SYY` + - `SYY`:步骤序号(`S01`、`S02`...) + +### 示例 +- 用例:`TC-FON-000018` +- 第3步:`TC-FON-000018-S03` + +## 3. 分配原则 + +1. 全项目共用一个递增序列,不按模块重置。 +2. 新增用例必须取“当前最大ID + 1”。 +3. 废弃用例保留ID,不复用。 +4. 若拆分用例,新增子用例使用新ID,不改旧ID。 + +## 4. 自动化报告字段(必须) + +每次自动化执行输出以下字段: + +- `run_id`:本次执行唯一ID(如时间戳) +- `test_case_id`:`TC-FON-XXXXXX` +- `step_id`:`TC-FON-XXXXXX-SYY` +- `status`:`passed` / `failed` / `blocked` / `skipped` +- `error_message`:失败信息 +- `actual_result`:实际结果 +- `expected_result`:预期结果 +- `screenshot_path`:失败截图(Web/E2E) +- `log_path`:后端日志/请求响应日志 +- `started_at` / `ended_at` + +## 5. 报告粒度要求 + +1. 报告必须能定位到**具体失败步骤**(step_id)。 +2. 汇总页至少包含: + - 总用例数、通过数、失败数、跳过数 + - 按模块统计(登录/房源/客源) + - Top失败步骤排行(按 step_id) +3. 详情页展示: + - 失败步骤前后 1~2 步执行上下文 + - 请求/响应(脱敏后) + - 关键断言差异(Expected vs Actual) + +## 6. 当前序列占用(本次) + +- 登录模块使用:`TC-FON-000001` ~ `TC-FON-000048` +- 下一可用ID:`TC-FON-000049` diff --git a/Project/fonrey/TEST_CASES/TEST_CASE_REGISTRY.md b/Project/fonrey/TEST_CASES/TEST_CASE_REGISTRY.md new file mode 100644 index 00000000..8639f1c2 --- /dev/null +++ b/Project/fonrey/TEST_CASES/TEST_CASE_REGISTRY.md @@ -0,0 +1,77 @@ +# Fonrey 测试用例编号注册表(全局唯一) + +> 用途:统一管理全项目测试用例编号,避免撞号,支持自动化报告追踪。 +> 适用范围:登录、房源、客源、组织人事、权限、系统设置等全部模块。 +> 编号规范:见 `TEST_CASES/TEST_CASE_ID_SPEC.md` + +--- + +## 1) 全局规则(强制) + +1. 全项目共用一个递增序列:`TC-FON-XXXXXX`。 +2. 不按模块重置编号。 +3. 新增用例必须先在本注册表登记后再写代码。 +4. 废弃用例保留编号,不得复用。 +5. 拆分/重构用例时,新用例使用新编号,旧编号可标记为 `deprecated`。 + +--- + +## 2) 当前编号水位 + +- **已分配到**:`TC-FON-000048` +- **下一个可用编号**:`TC-FON-000049` +- **最后更新人**:Atlas +- **最后更新时间**:2026-04-30 + +> 说明:下一个新增用例(不论哪个模块)都应从 `TC-FON-000049` 开始。 + +--- + +## 3) 编号段注册总览(按批次) + +| 批次ID | 模块 | 编号范围 | 数量 | 状态 | 文档 | +|---|---|---:|---:|---|---| +| BATCH-LOGIN-001 | 登录模块 | TC-FON-000001 ~ TC-FON-000048 | 48 | active | `TEST_CASES/TEST_CASES_LOGIN_MODULE.md` | + +**状态枚举**: +- `active`:有效且执行中 +- `deprecated`:已废弃但保留追溯 +- `reserved`:已预留待落地 + +--- + +## 4) 逐号注册明细(可选,按需扩展) + +> 当前先采用“编号段注册”。若后续需要逐号追踪,可在本节追加明细表。 + +| test_case_id | 模块 | 标题 | 状态 | 首次版本 | 备注 | +|---|---|---|---|---|---| +| TC-FON-000001 | 登录 | Tenant Code 页面首启展示 | active | v1.0 | 见登录用例文档 | +| TC-FON-000048 | 登录 | 验证码登录成功/失败/锁定限制 | active | v1.0 | 见登录用例文档 | + +--- + +## 5) 新增编号操作流程(团队统一) + +1. 打开本文件,查看“下一个可用编号”。 +2. 按需申请连续编号段(建议每次 5/10/20 条)。 +3. 在“编号段注册总览”新增一行,状态先标 `reserved`。 +4. 完成测试用例文档与代码后,改为 `active`。 +5. 同步更新“当前编号水位”。 + +--- + +## 6) 合并门禁建议(CI) + +建议在 CI 中增加校验: +- 检查是否存在重复 `TC-FON-XXXXXX` +- 检查编号是否小于等于当前水位且未登记 +- 检查新增用例是否已在本注册表存在对应编号段 + +--- + +## 7) 变更记录 + +| 日期 | 变更人 | 变更内容 | +|---|---|---| +| 2026-04-30 | Atlas | 初始化注册表;登记登录模块 000001~000048;下一号设为 000049 | diff --git a/Project/fonrey/UI_DESIGN/登录_UI.html b/Project/fonrey/UI_DESIGN/登录_UI.html index 86c3003f..c3992de7 100644 --- a/Project/fonrey/UI_DESIGN/登录_UI.html +++ b/Project/fonrey/UI_DESIGN/登录_UI.html @@ -3,7 +3,7 @@ - Fonrey 登录管理 · 静态原型 + Fonrey 登录管理 · Tenant 识别(Story 1) - +
@@ -69,369 +47,126 @@ Fonrey 房睿
-

面向经纪业务的
高密度工作台

+

欢迎使用 Fonrey 房睿

- 多租户隔离、角色权限控制、房客源高频操作一致体验。 - 本页面原型覆盖 Tenant 识别、账号密码登录、验证码验证、锁定与会话过期等 P0 场景。 + 首次启动客户端时,请先输入 12 位公司识别码完成租户识别。 + 识别成功后将自动进入该租户的登录页面。

-
多租户识别
-
12位 Tenant ID
+
识别码格式
+
12 位纯数字
安全策略
-
5次失败锁定30分钟
+
公共接口限流保护
+

Tenant 识别

+

请输入您公司的专属识别码以继续

-