diff --git a/Project/fonrey/PRD/TASK.md b/Project/fonrey/PRD/TASK.md new file mode 100644 index 00000000..ea983450 --- /dev/null +++ b/Project/fonrey/PRD/TASK.md @@ -0,0 +1,834 @@ +## Project Task Board + +### 项目状态总览 + +- 产品名称:Fonrey 房睿 +- 当前阶段:MVP Phase 1 +- 技术栈:Django 4.x + HTMX + Alpine.js + Tailwind CSS + PostgreSQL 16 + Redis + Celery + Cloudflare R2 +- 最后更新:2026-04-26 + +--- + +### Task 索引总览 + +> 点击 US 编号可直接跳转到对应 Task 详情。 + +#### Phase 1 — MVP(P0) + +| US 编号 | 模块 | Task 描述 | 状态 | +|---|---|---|---| +| [US-ACCOUNT-001](#US-ACCOUNT-001-经纪人管理员使用账号密码登录系统) | 用户登录 | 经纪人/管理员使用账号密码登录系统 | [ ] | +| [US-ACCOUNT-002](#US-ACCOUNT-002-系统识别多租户子域名域名路由) | 用户登录 | 系统识别多租户(子域名/域名路由) | [ ] | +| [US-ACCOUNT-003](#US-ACCOUNT-003-系统管理-Token-与会话超时) | 用户登录 | 系统管理 Token 与会话超时 | [ ] | +| [US-COMPLEX-001](#US-COMPLEX-001-管理员录入与维护楼盘基础信息) | 楼盘管理 | 管理员录入与维护楼盘基础信息 | [ ] | +| [US-COMPLEX-002](#US-COMPLEX-002-经纪人查看楼盘列表与详情) | 楼盘管理 | 经纪人查看楼盘列表与详情 | [ ] | +| [US-COMPLEX-003](#US-COMPLEX-003-管理员维护区域管理城区商圈) | 楼盘管理 | 管理员维护区域管理(城区/商圈) | [ ] | +| [US-PROPERTY-001](#US-PROPERTY-001-经纪人录入二手住宅出售出租) | 房源管理 | 经纪人录入二手住宅(出售/出租) | [ ] | +| [US-PROPERTY-002](#US-PROPERTY-002-经纪人查看与筛选房源列表) | 房源管理 | 经纪人查看与筛选房源列表 | [ ] | +| [US-PROPERTY-003](#US-PROPERTY-003-经纪人查看房源详情页) | 房源管理 | 经纪人查看房源详情页 | [ ] | +| [US-PROPERTY-004](#US-PROPERTY-004-经纪人写入与查看房源跟进记录) | 房源管理 | 经纪人写入与查看房源跟进记录 | [ ] | +| [US-PROPERTY-005](#US-PROPERTY-005-经纪人管理房源图片上传分类排序) | 房源管理 | 经纪人管理房源图片(上传/分类/排序) | [ ] | +| [US-PROPERTY-006](#US-PROPERTY-006-经纪人管理业主联系人) | 房源管理 | 经纪人管理业主联系人 | [ ] | +| [US-PROPERTY-007](#US-PROPERTY-007-经纪人调整房源价格) | 房源管理 | 经纪人调整房源价格 | [ ] | +| [US-PROPERTY-008](#US-PROPERTY-008-经纪人变更房源状态) | 房源管理 | 经纪人变更房源状态 | [ ] | +| [US-CLIENT-001](#US-CLIENT-001-经纪人录入新私客) | 客源管理 | 经纪人录入新私客 | [ ] | +| [US-CLIENT-002](#US-CLIENT-002-经纪人查看与筛选私客列表全部求购求租) | 客源管理 | 经纪人查看与筛选私客列表(全部/求购/求租) | [ ] | +| [US-CLIENT-003](#US-CLIENT-003-经纪人批量操作私客列表) | 客源管理 | 经纪人批量操作私客列表 | [ ] | +| [US-CLIENT-004](#US-CLIENT-004-经纪人查看私客详情页) | 客源管理 | 经纪人查看私客详情页 | [ ] | +| [US-CLIENT-005](#US-CLIENT-005-经纪人查看与编辑需求信息) | 客源管理 | 经纪人查看与编辑需求信息 | [ ] | +| [US-CLIENT-006](#US-CLIENT-006-经纪人写入与查看跟进记录) | 客源管理 | 经纪人写入与查看跟进记录 | [ ] | +| [US-CLIENT-007](#US-CLIENT-007-经纪人管理带看记录预约带看新增带看) | 客源管理 | 经纪人管理带看记录(预约带看/新增带看) | [ ] | +| [US-CLIENT-008](#US-CLIENT-008-经纪人管理客源联系人查看新增编辑) | 客源管理 | 经纪人管理客源联系人(查看/新增/编辑) | [ ] | +| [US-CLIENT-009](#US-CLIENT-009-经纪人修改客源等级) | 客源管理 | 经纪人修改客源等级 | [ ] | +| [US-CLIENT-010](#US-CLIENT-010-经纪人修改客源状态) | 客源管理 | 经纪人修改客源状态 | [ ] | +| [US-CLIENT-011](#US-CLIENT-011-经纪人手动将私客转为公客) | 客源管理 | 经纪人手动将私客转为公客 | [ ] | +| [US-CLIENT-012](#US-CLIENT-012-经纪人将私客转为成交客) | 客源管理 | 经纪人将私客转为成交客 | [ ] | +| [US-CLIENT-013](#US-CLIENT-013-经纪人将客源标记为无效) | 客源管理 | 经纪人将客源标记为无效 | [ ] | +| [US-CLIENT-014](#US-CLIENT-014-经纪人编辑客源完整信息联系人基础信息需求) | 客源管理 | 经纪人编辑客源完整信息(联系人/基础信息/需求) | [ ] | +| [US-CLIENT-015](#US-CLIENT-015-经纪人管理客源相关员工查看编辑归属人首录人) | 客源管理 | 经纪人管理客源相关员工(查看/编辑归属人/首录人) | [ ] | +| [US-CLIENT-016](#US-CLIENT-016-系统自动将超时无跟进的私客转为公客) | 客源管理 | 系统自动将超时无跟进的私客转为公客 | [ ] | +| [US-CLIENT-017](#US-CLIENT-017-系统自动检测重复客源并提示) | 客源管理 | 系统自动检测重复客源并提示 | [ ] | +| [US-ORG-001](#US-ORG-001-管理员维护公司组织结构部门门店树) | 组织人事 | 管理员维护公司组织结构(部门/门店树) | [ ] | +| [US-ORG-002](#US-ORG-002-管理员查看与维护员工列表) | 组织人事 | 管理员查看与维护员工列表 | [ ] | +| [US-ORG-003](#US-ORG-003-管理员办理员工入职并创建系统账号) | 组织人事 | 管理员办理员工入职并创建系统账号 | [ ] | +| [US-PERMISSION-001](#US-PERMISSION-001-管理员配置角色预设角色自定义角色) | 权限管理 | 管理员配置角色(预设角色/自定义角色) | [ ] | +| [US-PERMISSION-002](#US-PERMISSION-002-管理员查看与管理人员权限列表) | 权限管理 | 管理员查看与管理人员权限列表 | [ ] | +| [US-PERMISSION-003](#US-PERMISSION-003-管理员批量为员工分配角色) | 权限管理 | 管理员批量为员工分配角色 | [ ] | +| [US-PERMISSION-004](#US-PERMISSION-004-系统执行功能权限控制菜单级) | 权限管理 | 系统执行功能权限控制(菜单级) | [ ] | +| [US-PERMISSION-005](#US-PERMISSION-005-系统执行数据权限控制部门个人全司) | 权限管理 | 系统执行数据权限控制(部门/个人/全司) | [ ] | +| [US-SETTING-001](#US-SETTING-001-管理员配置房源相关设置字段必填自定义字段标签) | 系统配置 | 管理员配置房源相关设置(字段必填/自定义字段/标签) | [ ] | + +#### Phase 2 — 增强功能(P1) + +| US 编号 | 模块 | Task 描述 | 状态 | +|---|---|---|---| +| [US-ACCOUNT-010](#US-ACCOUNT-010-经纪人使用短信验证码登录) | 用户登录 | 经纪人使用短信验证码登录 | [ ] | +| [US-ACCOUNT-011](#US-ACCOUNT-011-经纪人重置账号密码) | 用户登录 | 经纪人重置账号密码 | [ ] | +| [US-ACCOUNT-012](#US-ACCOUNT-012-系统记住用户登录状态) | 用户登录 | 系统记住用户登录状态 | [ ] | +| [US-COMPLEX-010](#US-COMPLEX-010-管理员管理楼盘照片) | 楼盘管理 | 管理员管理楼盘照片 | [ ] | +| [US-COMPLEX-011](#US-COMPLEX-011-管理员维护楼盘价格走势) | 楼盘管理 | 管理员维护楼盘价格走势 | [ ] | +| [US-COMPLEX-012](#US-COMPLEX-012-管理员维护周边配套学校管理) | 楼盘管理 | 管理员维护周边配套(学校管理) | [ ] | +| [US-PROPERTY-010](#US-PROPERTY-010-经纪人查看房源维护完成度诊断面板) | 房源管理 | 经纪人查看房源维护完成度(诊断面板) | [ ] | +| [US-PROPERTY-011](#US-PROPERTY-011-经纪人管理敏感信息跟进权限控制) | 房源管理 | 经纪人管理敏感信息跟进(权限控制) | [ ] | +| [US-PROPERTY-012](#US-PROPERTY-012-经纪人管理房源附件) | 房源管理 | 经纪人管理房源附件 | [ ] | +| [US-PROPERTY-013](#US-PROPERTY-013-经纪人查看房源市场报盘) | 房源管理 | 经纪人查看房源市场报盘 | [ ] | +| [US-PROPERTY-014](#US-PROPERTY-014-经纪人查看房源价格解读) | 房源管理 | 经纪人查看房源价格解读 | [ ] | +| [US-CLIENT-020](#US-CLIENT-020-经纪人使用二手配房功能查看匹配房源) | 客源管理 | 经纪人使用二手配房功能查看匹配房源 | [ ] | +| [US-CLIENT-021](#US-CLIENT-021-经纪人查看客源解读AI行为分析) | 客源管理 | 经纪人查看客源解读(AI行为分析) | [ ] | +| [US-CLIENT-022](#US-CLIENT-022-经纪人将重点客源收藏至收藏夹) | 客源管理 | 经纪人将重点客源收藏至收藏夹 | [ ] | +| [US-CLIENT-023](#US-CLIENT-023-经纪人通过快捷入口编辑客源基础信息) | 客源管理 | 经纪人通过快捷入口编辑客源基础信息 | [ ] | +| [US-CLIENT-024](#US-CLIENT-024-经纪人查看客源操作日志) | 客源管理 | 经纪人查看客源操作日志 | [ ] | +| [US-ORG-010](#US-ORG-010-管理员办理员工离职与调动) | 组织人事 | 管理员办理员工离职与调动 | [ ] | +| [US-ORG-011](#US-ORG-011-管理员维护员工通讯录) | 组织人事 | 管理员维护员工通讯录 | [ ] | +| [US-ORG-012](#US-ORG-012-管理员管理员工职务) | 组织人事 | 管理员管理员工职务 | [ ] | +| [US-PERMISSION-010](#US-PERMISSION-010-管理员配置字段级权限敏感字段可见性) | 权限管理 | 管理员配置字段级权限(敏感字段可见性) | [ ] | +| [US-PERMISSION-011](#US-PERMISSION-011-管理员配置个人特定权限覆盖) | 权限管理 | 管理员配置个人特定权限覆盖 | [ ] | +| [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-RELEASE-010](#US-RELEASE-010-系统发布Windows桌面客户端安装包) | 客户端发布 | 系统发布Windows桌面客户端安装包 | [ ] | +| [US-RELEASE-011](#US-RELEASE-011-客户端自动检测并更新至最新版本) | 客户端发布 | 客户端自动检测并更新至最新版本 | [ ] | + +#### Phase 3 — 路线图功能(P2) + +| US 编号 | 模块 | Task 描述 | 状态 | +|---|---|---|---| +| [US-PROPERTY-020](#US-PROPERTY-020-经纪人录入别墅商铺商住写字楼其他类型房源) | 房源管理 | 经纪人录入别墅/商铺/商住/写字楼/其他类型房源 | [ ] | +| [US-PROPERTY-021](#US-PROPERTY-021-经纪人查看全部商铺写字楼列表) | 房源管理 | 经纪人查看全部商铺/写字楼列表 | [ ] | +| [US-COMPLEX-020](#US-COMPLEX-020-管理员使用应用数据标准功能) | 楼盘管理 | 管理员使用应用数据标准功能 | [ ] | +| [US-CLIENT-030](#US-CLIENT-030-经纪人查看与管理公客列表) | 客源管理 | 经纪人查看与管理公客列表 | [ ] | +| [US-CLIENT-031](#US-CLIENT-031-经纪人查看与管理成交客列表) | 客源管理 | 经纪人查看与管理成交客列表 | [ ] | +| [US-CLIENT-032](#US-CLIENT-032-经纪人管理暂缓私客) | 客源管理 | 经纪人管理暂缓私客 | [ ] | +| [US-ORG-020](#US-ORG-020-管理员查看员工异动记录) | 组织人事 | 管理员查看员工异动记录 | [ ] | +| [US-ORG-021](#US-ORG-021-管理员管理员工奖惩记录) | 组织人事 | 管理员管理员工奖惩记录 | [ ] | +| [US-ORG-022](#US-ORG-022-管理员查看门店分布地图) | 组织人事 | 管理员查看门店分布地图 | [ ] | +| [US-SETTING-020](#US-SETTING-020-管理员配置人事OA相关参数) | 系统配置 | 管理员配置人事OA相关参数 | [ ] | +| [US-SETTING-021](#US-SETTING-021-管理员配置交易规则) | 系统配置 | 管理员配置交易规则 | [ ] | +| [US-SETTING-022](#US-SETTING-022-管理员配置财务规则) | 系统配置 | 管理员配置财务规则 | [ ] | +| [US-SETTING-023](#US-SETTING-023-管理员配置合同模板) | 系统配置 | 管理员配置合同模板 | [ ] | +| [US-SYSTEM-020](#US-SYSTEM-020-平台管理员查看操作审计日志) | 系统管理 | 平台管理员查看操作审计日志 | [ ] | +| [US-SYSTEM-021](#US-SYSTEM-021-平台管理员管理灰度发布滚动升级) | 系统管理 | 平台管理员管理灰度发布/滚动升级 | [ ] | + +--- + +## Phase 1 - MVP(P0,上线前必须完成) + +--- + +### 用户登录 + +##### US-ACCOUNT-001 经纪人管理员使用账号密码登录系统 + +- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 账号密码登录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/登录管理/登录_UI.md` +- 状态:[ ] +- 验收标准:输入正确账号密码后跳转首页;密码错误时展示"账号或密码错误"提示;连续错误5次后账号锁定提示;登录成功后 Token 写入 Cookie + +##### US-ACCOUNT-002 系统识别多租户子域名域名路由 + +- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 多租户识别 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +- 状态:[ ] +- 验收标准:访问不同子域名时系统自动切换对应租户 Schema;非法/不存在子域名返回404页面;跨租户请求被拦截并返回403 + +##### US-ACCOUNT-003 系统管理 Token 与会话超时 + +- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - Token 管理/会话超时 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +- 状态:[ ] +- 验收标准:会话超时后自动跳转登录页;Token 刷新机制正常工作;登出后 Token 立即失效,再次请求跳转登录页 + +--- + +### 楼盘管理 + +##### US-COMPLEX-001 管理员录入与维护楼盘基础信息 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘信息管理 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/楼盘管理/楼盘详情_UI.md` +- 状态:[ ] +- 验收标准:可录入楼盘名称、地址、楼栋、结构等基础字段;保存成功后楼盘出现在楼盘列表;必填字段未填时高亮错误提示;楼盘编号系统自动生成且唯一 + +##### US-COMPLEX-002 经纪人查看楼盘列表与详情 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘列表/楼盘详情 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/楼盘管理/楼盘列表_UI.md` +- 状态:[ ] +- 验收标准:楼盘列表支持按名称/地址关键词搜索;支持分页(默认20条/页);点击楼盘名称跳转详情页;详情页展示楼盘基本信息、楼栋结构信息 + +##### US-COMPLEX-003 管理员维护区域管理城区商圈 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 区域管理 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md` +- 状态:[ ] +- 验收标准:可新增/编辑/删除城区和商圈;商圈必须归属于城区;区域数据被房源和客源模块正确关联引用;删除有关联数据的区域时系统给出警告 + +--- + +### 房源管理 + +##### US-PROPERTY-001 经纪人录入二手住宅出售出租 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 录入住宅(二手出售/出租) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/新增房源_UI.md` +- 状态:[ ] +- 验收标准:可在3分钟内完成住宅基本信息录入;必填字段(楼盘/楼层/面积/价格)未填时高亮错误提示;保存成功后跳转房源详情页并显示"保存成功";录入后即刻出现在房源列表 + +##### US-PROPERTY-002 经纪人查看与筛选房源列表 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源列表(二手&租赁) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/房源列表_UI.md` +- 状态:[ ] +- 验收标准:列表支持按楼盘名/业主姓名/电话/房源编号关键词搜索;支持状态/区域/价格/房型多维度组合筛选;列表分页(默认20条/页)且89000条数据下查询响应<2秒;支持导出当前筛选结果为Excel + +##### US-PROPERTY-003 经纪人查看房源详情页 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源详情页 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/房源管理/房源详情_UI.md` +- 状态:[ ] +- 验收标准:详情页完整展示基本信息、产证信息、交易信息;号码默认打码,点击"查看号码"后解密展示并记录审计日志;页面各功能 Tab 可正常切换 + +##### US-PROPERTY-004 经纪人写入与查看房源跟进记录 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 跟进记录(全部/写入/修改/其他) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:跟进记录按时间线倒序展示;支持写入跟进/修改跟进/其他跟进(钥匙/委托/实勘)子Tab切换;跟进内容最少6字校验;写入成功后记录实时出现在列表顶部 + +##### US-PROPERTY-005 经纪人管理房源图片上传分类排序 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 图片管理(相册上传/分类/排序) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:支持上传 bmp/jpg/png/gif 格式图片,单文件最大20MB;上传成功后自动按分类展示;支持拖拽排序;封面图可手动指定 + +##### US-PROPERTY-006 经纪人管理业主联系人 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 业主联系人管理 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:支持新增/编辑业主联系人(姓名/电话/微信);手机号加密存储,展示时默认打码;可查看同业主名下其他房源;至少保留一个联系人 + +##### US-PROPERTY-007 经纪人调整房源价格 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 价格调整(调价/调价记录) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:调价弹窗需填写新价格和调价原因;调价成功后房源列表和详情页价格实时更新;调价记录以时间线形式留存且不可删除;调价幅度在列表中以"降价XX万"标签展示 + +##### US-PROPERTY-008 经纪人变更房源状态 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源状态变更(在售/暂缓/成交/下架) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:状态变更严格遵循状态机流转规则(在售→暂缓/成交/下架);每次状态变更需填写原因;状态变更后列表状态标签实时更新;状态变更记录写入跟进日志 + +--- + +### 客源管理 + +##### US-CLIENT-001 经纪人录入新私客 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 1:经纪人录入新私客;5.2 录入私客 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md` +- 状态:[ ] +- 验收标准:录入页面可通过顶部导航「客源」→「+新增私客」或右侧快捷入口「增客」触达;联系人1必填(姓名/性别/电话1),联系人2起可增加删除;基础信息必填字段(状态/用途/等级/来源)缺填时高亮错误并定位到第一个错误处;保存成功后跳转该客源详情页并显示"保存成功"提示 + +##### US-CLIENT-002 经纪人查看与筛选私客列表全部求购求租 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2/3/4:经纪人查看与筛选私客列表;5.1 客源列表 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md` +- 状态:[ ] +- 验收标准:顶部Tab导航(私客/资料客/营销客/成交客/公客)及二级Tab(求购/求租/暂缓/全部私客)可正常切换;搜索框支持按客源姓名/号码/号码后4位/客源编号/备注检索;筛选栏支持状态/等级/位置/价格/房室等多维度组合筛选;列表底部实时显示当前筛选总条数,分页默认20条/页,89000条数据量下查询<2秒 + +##### US-CLIENT-003 经纪人批量操作私客列表 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2:列表批量操作;5.1.3 批量操作 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md` +- 状态:[ ] +- 验收标准:勾选客源后激活批量操作按钮(修改相关方/修改来源/删除客源/合并客户);批量删除执行软删除,可在"已删客源"中查看;批量修改相关方成功后列表归属人字段实时刷新;列表支持导出当前筛选结果为Excel(Celery异步处理) + +##### US-CLIENT-004 经纪人查看私客详情页 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 6:经纪人查看私客详情页;Story 15:经纪人查看客源信息概览面板 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:详情页顶部展示需求标题+联系人姓名+带看进度标签;右侧固定信息概览面板展示客户编号/委托日期/需求类型等字段且不随页面滚动消失;主内容区Tab导航(需求信息/跟进记录/带看/客源解读/智能配房)默认激活"需求信息"Tab;右侧面板三个主操作按钮(打电话/写跟进/报备带看)可正常触发对应流程 + +##### US-CLIENT-005 经纪人查看与编辑需求信息 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 7:经纪人查看与编辑需求信息 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:需求信息Tab展示总价/面积/居室/装修/朝向/楼层/楼龄/意向商圈/意向小区等字段(三栏布局);字段值为空时显示"-"占位符;右上角「编辑」链接点击后字段转为输入框/选择器;保存成功后返回详情页并刷新需求信息区块 + +##### US-CLIENT-006 经纪人写入与查看跟进记录 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 8:经纪人写入与查看跟进记录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:跟进记录分5个子Tab(全部/写入跟进/敏感信息跟进/修改跟进/其他跟进);全部Tab支持时间范围筛选及「有录音」「有图片」快速过滤;跟进记录以时间线形式按日期分组倒序展示;写入跟进时跟进目的支持23项多选,跟进内容最少6字校验;系统自动生成的操作日志(如新增私客/状态变更)出现在"其他跟进"Tab + +##### US-CLIENT-007 经纪人管理带看记录预约带看新增带看 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 9:经纪人管理带看记录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:带看Tab分"预约"和"带看"两个子Tab;新增带看表单必填字段(带看时间/带看情况≥6字/带看房源≥1套)校验通过才可提交;带看记录以时间线展示,含带看房源蓝色可点击链接和带看进度标签;员工选择器弹层支持组织树搜索和多选;带看房源选择器支持按编号/楼盘/业主关键词搜索 + +##### US-CLIENT-008 经纪人管理客源联系人查看新增编辑 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 23:经纪人管理客源联系人 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:联系人面板默认打码显示电话("+86 135\*\*\*\*\*\*\*\*"),点击「查看号码」后展示完整号码并写入敏感信息跟进日志;新增联系人必填字段(姓名/称呼/电话1)校验;编辑联系人时电话1需点击「查看号码」后才可编辑;保存成功后联系人面板实时刷新 + +##### US-CLIENT-009 经纪人修改客源等级 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 17:经纪人修改客源等级 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击「改等级」触发弹窗,展示当前等级(只读)和新等级下拉选择器(A急迫/B较强/C一般/D较弱/E暂不关注);新等级未选择时「确定」按钮置灰;保存成功后信息概览面板等级标签实时更新;操作日志中自动新增"改等级"记录 + +##### US-CLIENT-010 经纪人修改客源状态 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 18:经纪人修改客源状态 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击「改状态」触发弹窗,展示当前状态(只读)、新状态下拉(求购/求租/租购)、等级下拉和必填更改理由文本框;新状态未选或理由未填时「确定」按钮置灰;保存成功后信息概览面板状态标签实时更新;操作日志新增"改状态"记录(含更改理由) + +##### US-CLIENT-011 经纪人手动将私客转为公客 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 19:经纪人手动将私客转为公客 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击「转公客」触发弹窗,状态和等级均为必填;确认后客源从私客列表移除并进入公客池(client_type='public',transfer_to_public_type='manual');操作日志新增"转公客"记录(含操作人/操作时间);权限控制:仅归属人、首录人或有管理权限的店长/经理可操作 + +##### US-CLIENT-012 经纪人将私客转为成交客 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 20:经纪人将私客转为成交客 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击「转成交」触发"客户成交"弹窗,必填字段(状态/房源类型/成交房源/成交日期/成交价格/成交方)全部填写后「确定」按钮高亮;成交房源选择浮层支持关键词搜索和区域/状态筛选;提交后客源移入成交客列表,状态更新为"成交";操作日志新增"转成交"记录(含成交信息摘要) + +##### US-CLIENT-013 经纪人将客源标记为无效 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 21:经纪人将客源标记为无效 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击「转无效」触发弹窗,展示蓝色提示框说明"将把所有电话标记无效",无效原因单选(号码无效/同行中介/广告推销/客户无意向/其他)默认选中"号码无效";确认后所有联系人电话标记为无效,客源从私客活跃列表移除;操作日志新增"转无效"记录(含无效原因);权限控制:仅归属人、首录人或有管理权限者可操作 + +##### US-CLIENT-014 经纪人编辑客源完整信息联系人基础信息需求 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 14:经纪人编辑客源信息 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md` +- 状态:[ ] +- 验收标准:编辑页面分三个Tab(联系人/基础信息/二手或新房或租房);联系人Tab的电话1需点击「查看号码」后才可编辑,旁边提供「标记无效」链接;基础信息Tab包含需求类型/购房目的/付款方式/名下房产/贷款记录等扩展字段;保存时校验所有必填字段,成功后返回详情页并刷新需求信息区块 + +##### US-CLIENT-015 经纪人管理客源相关员工查看编辑归属人首录人 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 24:经纪人管理客源相关员工 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:相关员工面板展示首录人/归属人的所属门店+小组+姓名及参与时间;点击「编辑」触发弹窗,首录人和归属人均为必填下拉选择器(支持搜索姓名);保存后面板实时更新,操作日志新增"修改相关员工"记录;权限控制:跨团队/跨店修改需店长及以上权限 + +##### US-CLIENT-016 系统自动将超时无跟进的私客转为公客 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - 关键业务规则:私客自动转公 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 状态:[ ] +- 验收标准:Celery Beat 定时任务每日凌晨执行;超过运营配置天数(如30天)无跟进且非保护客的私客自动转入公客池(transfer_to_public_type='auto');自动转公后 client_status_logs 生成一条 to_public 记录;即将过期的私客在列表中显示"即将掉公"提示标签 + +##### US-CLIENT-017 系统自动检测重复客源并提示 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2:顶部重复检测提示;关键业务规则:私客手机号唯一性 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 状态:[ ] +- 验收标准:录入/编辑联系人手机号时实时通过phone_hash检测与现有私客/成交客/公客的重复;客源列表顶部实时显示"私客与成交客重复:XX"和"私客与公客重复:XX"蓝色可点击链接;点击重复数字链接可查看重复名单 + +--- + +### 组织人事 + +##### US-ORG-001 管理员维护公司组织结构部门门店树 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 公司组织结构(部门/门店树) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:支持新增/编辑/删除部门和门店节点;组织树以层级结构展示(公司→区域→门店→小组);删除有员工的部门时系统提示并阻止操作;组织结构变更实时反映在员工选择器弹层中 + +##### US-ORG-002 管理员查看与维护员工列表 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工列表/员工详情 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:员工列表支持按姓名/手机号关键词搜索;支持按部门/状态筛选;列表展示员工姓名/所属门店/职位/状态等字段;点击员工姓名跳转员工详情页 + +##### US-ORG-003 管理员办理员工入职并创建系统账号 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工入职/账号创建 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:入职表单必填字段(姓名/手机号/所属门店/职位)校验;创建账号后系统自动生成登录密码并可发送给员工;新员工账号立即可登录系统;员工账号与组织树节点正确关联 + +--- + +### 权限管理 + +##### US-PERMISSION-001 管理员配置角色预设角色自定义角色 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 角色管理(预设角色+自定义角色) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:系统预设角色(经纪人/店长/管理员等)不可删除但可复制;支持创建自定义角色并配置功能权限;角色名称在同租户内唯一;删除自定义角色前需解除所有人员绑定 + +##### US-PERMISSION-002 管理员查看与管理人员权限列表 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 人员权限列表 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:人员权限列表展示员工姓名/所属部门/当前角色;支持按部门/角色筛选;列表支持分页;点击员工行可查看详细权限配置 + +##### US-PERMISSION-003 管理员批量为员工分配角色 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 角色批量分配 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:勾选多名员工后可批量指定角色;批量分配成功后所有选中员工角色立即生效;分配后员工下次登录或刷新页面权限即更新(Redis权限快照失效重载) + +##### US-PERMISSION-004 系统执行功能权限控制菜单级 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 功能权限(菜单级) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:无权限的菜单在导航中不展示;直接访问无权限URL返回403页面;权限变更后Redis缓存自动失效,用户下一次请求即应用新权限;经纪人无法访问管理员专属功能页 + +##### US-PERMISSION-005 系统执行数据权限控制部门个人全司 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 数据权限(部门/个人/全司) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:经纪人只能看到自己名下的房源和客源数据;店长可见本门店所有员工的房源和客源;管理员可见全司数据;不同数据权限级别的用户查询结果严格隔离,不可通过URL参数绕过 + +--- + +### 系统配置 + +##### US-SETTING-001 管理员配置房源相关设置字段必填自定义字段标签 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 房源设置(字段必填/自定义字段/标签) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md` +- 状态:[ ] +- 验收标准:可配置房源录入表单中哪些字段为必填;可新增自定义字段并在房源表单中展示;标签配置后可在房源筛选中使用;配置变更后房源录入表单实时生效 + +--- + +## Phase 2 - 增强功能(P1,MVP 后第一迭代) + +--- + +### 用户登录 + +##### US-ACCOUNT-010 经纪人使用短信验证码登录 + +- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 短信验证码登录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +- 状态:[ ] +- 验收标准:点击"发送验证码"后60秒内不可重复点击;验证码5分钟内有效;输入正确验证码后登录成功跳转首页;错误验证码给出明确提示 + +##### US-ACCOUNT-011 经纪人重置账号密码 + +- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 密码重置 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +- 状态:[ ] +- 验收标准:通过手机号验证码或邮箱链接验证身份;新密码需二次确认且满足强度要求(8位以上含字母+数字);重置成功后原会话立即失效需重新登录;重置操作写入审计日志 + +##### US-ACCOUNT-012 系统记住用户登录状态 + +- 参考PRD文档:`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 记住登录状态 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +- 状态:[ ] +- 验收标准:勾选"记住我"后关闭浏览器重新打开仍处于登录态;记住登录的有效期为7天;超过有效期自动跳转登录页;管理员可在后台强制让指定账号的所有会话失效 + +--- + +### 楼盘管理 + +##### US-COMPLEX-010 管理员管理楼盘照片 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘照片管理 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md` +- 状态:[ ] +- 验收标准:支持上传楼盘封面图和相册图片(格式jpg/png/gif,单文件≤20MB);上传成功后图片在楼盘详情页展示;支持删除图片;楼盘封面图可手动设置 + +##### US-COMPLEX-011 管理员维护楼盘价格走势 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘价格走势 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md` +- 状态:[ ] +- 验收标准:楼盘详情页可查看历史价格走势折线图;价格走势数据按月维度展示;支持手动录入或批量导入历史均价数据 + +##### US-COMPLEX-012 管理员维护周边配套学校管理 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 周边配套(学校管理) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md` +- 状态:[ ] +- 验收标准:可为楼盘关联周边学校(从学校库选择);学校库支持新增/编辑/删除;楼盘详情页展示关联学校列表;学校数据可在客源意向学校字段中搜索引用 + +--- + +### 房源管理 + +##### US-PROPERTY-010 经纪人查看房源维护完成度诊断面板 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源维护完成度(诊断面板) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:房源详情页展示数据完整度百分比及未填字段提示列表;点击未填字段提示可直接跳转对应编辑区域;完整度≥80%时展示绿色状态,<60%时展示红色警告 + +##### US-PROPERTY-011 经纪人管理敏感信息跟进权限控制 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 敏感信息跟进(查看权限控制) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:敏感信息跟进Tab仅对有权限的角色可见;查看敏感信息自动写入审计日志(不可删除);无权限用户看到Tab时显示"无权限查看"提示而非隐藏Tab + +##### US-PROPERTY-012 经纪人管理房源附件 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 附件管理 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:支持上传PDF/Word/Excel等文档类附件(单文件≤50MB);附件按上传时间倒序展示;支持下载和删除附件;附件上传通过Celery异步处理,不阻塞主线程 + +##### US-PROPERTY-013 经纪人查看房源市场报盘 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 市场报盘 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:房源详情页可查看同楼盘其他在售房源的报价分布;展示该楼盘近期成交均价和当前挂牌均价;数据按成交日期倒序展示 + +##### US-PROPERTY-014 经纪人查看房源价格解读 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 价格解读 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md` +- 状态:[ ] +- 验收标准:房源详情页展示基于同楼盘/同商圈数据的价格合理性分析;展示当前报价与参考价的偏差百分比;无足够数据时展示"暂无参考数据" + +--- + +### 客源管理 + +##### US-CLIENT-020 经纪人使用二手配房功能查看匹配房源 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 11:经纪人使用二手配房功能推荐房源 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:智能配房Tab分"录客配房"和"系统配房"两个子Tab;录客配房按优质户型/降价/热门/新上四个分组展示房源卡片;每套房源卡片展示封面图/小区名/户型/面积/售价/标签;点击「分享房源」触发房源分享流程;支持「批量分享」多套房源 + +##### US-CLIENT-021 经纪人查看客源解读AI行为分析 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 10:经纪人查看客源解读 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:客源解读Tab展示活跃行为/活跃时间/购房偏好三个模块;购房偏好支持近7日/近30日/近90日三个时间维度切换,切换后数据联动刷新;价格/户型/面积偏好以圆环图+图例形式展示;无数据时展示"暂无数据"而非报错 + +##### US-CLIENT-022 经纪人将重点客源收藏至收藏夹 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 16:经纪人收藏客源至私客收藏夹 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击「☆收藏」触发"选择私客收藏夹"浮层,默认选中"默认收藏夹";支持在浮层内创建新收藏夹(名称最多10字,超出「创建」按钮置灰);收藏成功后图标变为实心★橙色;私客列表支持按收藏夹筛选展示 + +##### US-CLIENT-023 经纪人通过快捷入口编辑客源基础信息 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 22:经纪人编辑客源基础信息(快捷入口) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 参考UI_Design文档:`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md` +- 状态:[ ] +- 验收标准:点击信息概览面板「编辑客源」入口触发"编辑基础信息"抽屉浮层;浮层包含需求类型/用途/来源(必填)及购房目的/付款方式/名下房产等选填字段;点击「确定」校验必填字段,保存成功后面板相关字段实时更新;点击「取消」或×关闭不保存 + +##### US-CLIENT-024 经纪人查看客源操作日志 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 25:经纪人查看客源操作日志 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 状态:[ ] +- 验收标准:点击「查看操作日志」跳转独立日志列表页(面包屑:客源/客源详情/客源操作日志);支持按日期范围/操作人/操作类型筛选;日志列表按操作时间倒序排列,展示操作时间/操作人/操作类型/操作内容;日志为只读,不支持编辑或删除;支持分页(默认20条/页) + +--- + +### 组织人事 + +##### US-ORG-010 管理员办理员工离职与调动 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工离职/调动 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:员工离职后账号立即禁用无法登录;离职员工名下房源和客源可批量转移给其他员工;员工调动后所属部门/组织信息实时更新;调动/离职操作写入异动记录 + +##### US-ORG-011 管理员维护员工通讯录 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工通讯录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:通讯录支持按姓名/部门搜索;展示员工姓名/职位/手机号/所属门店;手机号默认打码,有权限才可查看完整号码;支持按部门分组展示 + +##### US-ORG-012 管理员管理员工职务 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 职务管理 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:支持新增/编辑/删除职务(如经纪人/店长/区域经理);职务可关联权限角色;删除有在职员工绑定的职务时系统提示并阻止 + +--- + +### 权限管理 + +##### US-PERMISSION-010 管理员配置字段级权限敏感字段可见性 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 字段级权限(敏感字段可见性) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:可为不同角色配置联系人手机号/证件号码等敏感字段的可见性;无查看权限的角色访问时号码始终打码,无法点击查看;查看敏感字段操作记录在审计日志中;字段级权限配置变更后Redis缓存立即失效 + +##### US-PERMISSION-011 管理员配置个人特定权限覆盖 + +- 参考PRD文档:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 个人特定权限覆盖 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md` +- 状态:[ ] +- 验收标准:支持在角色权限基础上为特定员工单独开启或关闭特定权限点;个人覆盖权限优先级高于角色权限;个人权限覆盖配置记录可查看和删除 + +--- + +### 系统配置 + +##### US-SETTING-010 管理员配置首页展示内容 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 首页设置 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md` +- 状态:[ ] +- 验收标准:可配置首页展示的统计数据卡片(如今日新增房源/客源数量);配置变更后首页实时生效;不同角色可配置不同的首页视图 + +##### US-SETTING-011 管理员配置相关方规则 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 相关方设置 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md` +- 状态:[ ] +- 验收标准:可配置房源/客源的相关方角色(如协作人/跟进人)及其权限范围;相关方配置影响房源/客源详情页的相关员工区块展示 + +##### US-SETTING-012 管理员配置客源相关参数 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 客源设置(基本配置/参数配置) +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md` +- 状态:[ ] +- 验收标准:可配置私客自动转公客的天数阈值;可配置客源来源枚举值(lookup_items);可配置活跃度计算的各阈值天数;配置变更后Celery定时任务下次执行时使用新配置 + +--- + +### 系统管理(运营后台) + +##### US-SYSTEM-010 平台管理员管理租户开通暂停配置 + +- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 租户管理(开通/暂停/配置) +- 状态:[ ] +- 验收标准:可在运营后台新开通租户(自动创建独立PostgreSQL Schema);可暂停租户(暂停后租户用户无法登录);可为租户配置域名/子域名;租户操作记录写入平台操作日志 + +##### US-SYSTEM-011 平台管理员监控系统健康状态 + +- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 系统健康监控 +- 状态:[ ] +- 验收标准:运营后台展示系统核心指标(API响应时间/错误率/Celery队列积压);Sentry错误告警正常接收;Grafana面板可查看历史监控数据;关键指标超阈值时触发告警通知 + +--- + +### 客户端发布 + +##### US-RELEASE-010 系统发布Windows桌面客户端安装包 + +- 参考PRD文档:`Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md` - Windows桌面客户端 +- 状态:[ ] +- 验收标准:electron-builder 输出 NSIS .exe 安装包和便携版 .zip;安装包经EV证书签名,安装时无SmartScreen警告;安装包上传至Cloudflare R2并通过CDN分发;后端 ClientRelease 表新增一条版本记录 + +##### US-RELEASE-011 客户端自动检测并更新至最新版本 + +- 参考PRD文档:`Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md` - 自动更新机制 +- 状态:[ ] +- 验收标准:客户端启动时及每4小时自动检测 GET /api/client/updates/latest/ 接口;有新版本时后台静默下载,下载完成后提示用户重启;下载完成后校验SHA256与服务端返回一致才允许安装;强制更新标记时用户无法跳过更新 + +--- + +## Phase 3 - 路线图功能(P2,已规划未排期) + +--- + +### 房源管理 + +##### US-PROPERTY-020 经纪人录入别墅商铺商住写字楼其他类型房源 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 录入别墅/商铺/商住/写字楼/其他 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-PROPERTY-021 经纪人查看全部商铺写字楼列表 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 全部商铺列表/全部写字楼列表 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +--- + +### 楼盘管理 + +##### US-COMPLEX-020 管理员使用应用数据标准功能 + +- 参考PRD文档:`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 应用数据标准 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +--- + +### 客源管理 + +##### US-CLIENT-030 经纪人查看与管理公客列表 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 12:经纪人查看与筛选公客列表 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-CLIENT-031 经纪人查看与管理成交客列表 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 13:经纪人查看成交客列表 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-CLIENT-032 经纪人管理暂缓私客 + +- 参考PRD文档:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 5:经纪人管理暂缓私客 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md` +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +--- + +### 组织人事 + +##### US-ORG-020 管理员查看员工异动记录 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 异动记录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-ORG-021 管理员管理员工奖惩记录 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 奖惩记录 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-ORG-022 管理员查看门店分布地图 + +- 参考PRD文档:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 门店分布地图 +- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md` +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +--- + +### 系统配置 + +##### US-SETTING-020 管理员配置人事OA相关参数 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 人事OA设置 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-SETTING-021 管理员配置交易规则 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 交易设置 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-SETTING-022 管理员配置财务规则 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 财务设置 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-SETTING-023 管理员配置合同模板 + +- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置.md` - 合同设置 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +--- + +### 系统管理(运营后台) + +##### US-SYSTEM-020 平台管理员查看操作审计日志 + +- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 操作审计日志 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +##### US-SYSTEM-021 平台管理员管理灰度发布滚动升级 + +- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 灰度发布/滚动升级 +- 状态:[ ] +- 验收标准:(规划中,详细验收标准待PRD细化后补充) + +--- + +## Phase 4 - 明确不做(Out of Scope) + +> 以下功能在 MVP 阶段及近期版本路线图中明确不实现,仅作备忘。 + + + + + + + + + + + + + + + + + + +--- + +### 已完成 + +(暂无) diff --git a/Project/fonrey/PRD/房源管理/未命名.md b/Project/fonrey/PRD/房源管理/未命名.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md b/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md index 05082f4a..eda83556 100644 --- a/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md +++ b/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md @@ -1,648 +1,648 @@ -# PRD:用户登录管理模块 - -**状态**: Draft -**作者**: 产品经理 -**最后更新**: 2026-04-25(v1.4 §5.5 后端数据模型迁移至独立文档 `DATA_MODEL/DATA_MODEL_LOGIN.md`) -**版本**: 1.4 -**所属系统**: Fonrey 房产经纪管理系统 -**关联模块**: 组织人事管理、权限管理、系统管理 - ---- - -## 1. 问题陈述 - -### 1.1 背景 - -Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架构(`django-tenants` + PostgreSQL Schema 隔离)。终端用户通过 **Windows 桌面客户端(Electron)** 使用系统,无需手动输入网址即可打开 Web 应用。 - -在多租户环境下,用户的身份验证流程比单租户系统更复杂: - -- 用户安装客户端后,系统必须先识别当前设备归属哪个租户,才能加载对应租户的登录界面和数据隔离环境 -- 每家经纪公司作为独立租户,其员工账号、组织结构、数据均完全隔离 -- 经纪人账号须与实名员工档案绑定,确保每一条操作记录可追溯至具体自然人 -- 现阶段登录方式以账号密码为主;手机验证码登录、微信扫码登录需预留接口,待移动端(小程序)上线后实现 - -### 1.2 核心痛点 - -| 痛点 | 影响方 | 当前代价 | -|------|--------|---------| -| 多租户环境下,客户端不知道应该连接哪个租户的服务端 | 新用户首次安装后无法正常使用 | 系统无法启动,用户体验极差 | -| 账号密码裸露登录,缺乏验证码保护 | 所有用户 | 存在暴力破解、自动化恶意登录风险 | -| 用户忘记账号或密码无自助找回通道 | 一线经纪人 | 依赖管理员手动重置,效率低 | -| 账号未与实名经纪人档案绑定 | 系统管理员、合规审计 | 操作行为无法追溯至自然人 | -| 无多因素认证,安全系数低 | 管理层、数据合规 | 存在账号冒用、数据泄露风险 | - -### 1.3 目标用户 - -| 角色 | 描述 | 使用频率 | -|------|------|----------| -| 一线经纪人 | 每日登录系统使用房源/客源功能 | 每日高频 | -| 店长 / 经理 | 登录后查看全店数据、管理任务 | 每日 | -| 系统管理员 | 管理账号创建、密码重置、租户初始化 | 按需 | -| 新安装用户 | 首次安装客户端后需完成 Tenant 识别 | 一次性 | - ---- - -## 2. 目标与成功指标 - -| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 | -|------|------|----------|--------|----------| -| 降低登录失败率 | 账号密码正确情况下的登录成功率 | 待统计 | ≥ 99% | 上线后 30 天 | -| 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0(无保护) | 建立基线,同IP异常次数 > 5次/分钟触发封锁 | 上线后持续监控 | -| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约1工作日) | < 3 分钟(邮件/短信自助) | 上线后 30 天 | -| 确保账号实名绑定率 | 拥有系统账号且未与员工档案绑定的账号比例 | 待统计 | 0%(强制绑定) | 上线即达标 | -| Tenant 识别成功率 | 首次安装后成功完成 Tenant 识别的用户比例 | 待统计 | ≥ 98% | 上线后 30 天 | - ---- - -## 3. 非目标(本期不做) - -- **手机验证码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示 -- **微信扫码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示 -- **单点登录(SSO)/ 企业微信集成**:后续版本规划 -- **多设备并发登录的强制踢出策略**:本期允许同账号多端登录,后续安全策略模块规划 -- **登录时段限制 / IP 白名单**:安全策略模块另行规划 -- **管理后台(Platform Admin)登录**:系统管理员登录管理后台的流程属于系统管理模块,本 PRD 专注租户内用户登录 - ---- - -## 4. 用户故事与验收标准 - ---- - -### Story 1:新用户首次启动客户端——Tenant 识别 - -**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 Tenant ID 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。 - -**验收标准**: - -- [ ] 客户端首次启动时(本地无 Tenant ID 缓存),自动呈现「Tenant 识别」界面,而非直接显示登录界面 -- [ ] 界面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant ID 输入框、「确认」按钮 -- [ ] Tenant ID 输入框支持粘贴操作,自动去除前后空格 -- [ ] 点击「确认」后,客户端向服务端发起 Tenant 验证请求(`POST /api/auth/tenant/verify/`),展示加载状态(spinner) -- [ ] **验证成功**:服务端返回租户名称及品牌信息(如公司名称、Logo URL);客户端将 Tenant ID 写入本地持久化存储,自动跳转至该租户的登录界面;界面顶部展示「正在登录:XX 房产」 -- [ ] **验证失败(Tenant ID 无效)**:输入框下方显示红色错误提示「识别码无效,请联系您的系统管理员获取正确的识别码」;Tenant ID 不写入本地缓存;用户可重新输入 -- [ ] **网络异常**:显示「网络连接失败,请检查网络后重试」,提供「重试」按钮 -- [ ] 非首次启动(本地已有合法 Tenant ID 缓存):直接跳过识别界面,进入登录界面 -- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA),点击后清除本地 Tenant ID 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」 -- [ ] Tenant 验证接口属于公开接口,无需鉴权;但需对单 IP 请求频率限制(每分钟 ≤ 10 次)以防止枚举攻击 - ---- - -### Story 2:经纪人通过账号密码登录 - -**As** 已识别租户的经纪人,**I want** 通过用户名和密码完成登录,**So that** 进入系统开始工作。 - -**验收标准**: - -- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、用户名输入框、密码输入框、滑块拼图验证区域、「登录」按钮 -- [ ] 用户名输入框 Placeholder:「请输入用户名」;支持英文字母、数字、下划线,最大长度 50 字符 -- [ ] 密码输入框默认密文显示,右侧提供「显示/隐藏」图标切换明密文 -- [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符,操作直观快速 -- [ ] 验证逻辑:前端记录滑动轨迹(坐标序列 + 耗时),与背景图缺口位置一同发送至服务端;服务端综合校验**位置偏差**(允许 ±5px 容差)和**轨迹特征**(是否存在人类滑动的加速/减速规律)以区分机器行为 -- [ ] 验证失败(位置不准或轨迹异常):拼图区域抖动动画提示失败,自动刷新新的背景图,用户重新拖动;**不计入账号密码错误次数** -- [ ] 验证成功后,拼图区域显示绿色对勾 + 「验证通过」文案,状态持续至本次登录提交完成 -- [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图(针对图片模糊或缺口不清晰的情况) -- [ ] 背景图从预置图库中随机抽取,缺口位置每次随机生成,防止固定模式被预测 -- [ ] 三项(用户名、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰) -- [ ] 点击「登录」触发前端格式校验: - - 用户名为空 → 输入框下方红色提示「请输入用户名」 - - 密码为空 → 提示「请输入密码」 - - 验证码为空 → 提示「请输入验证码」 -- [ ] 格式校验通过后,向服务端发起登录请求,按钮进入 loading 状态防止重复提交 -- [ ] **登录成功**:服务端返回 Session Token;客户端存储 Token;跳转至系统首页;顶部显示欢迎信息「欢迎回来,{姓名}」 -- [ ] **登录失败(用户名或密码错误)**:显示「用户名或密码错误,请重新输入」(不区分是用户名错误还是密码错误,防止枚举攻击);验证码自动刷新;密码输入框清空;用户名保留 -- [ ] **登录失败(验证码错误)**:显示「验证码有误,请重新输入」;验证码自动刷新;验证码输入框清空 -- [ ] **账号被锁定**(同一账号密码连续错误 ≥ 5 次):显示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」;锁定状态下「登录」按钮置灰 -- [ ] **账号已停用**:显示「账号已停用,请联系您的管理员」 -- [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录界面,并提示「登录已过期,请重新登录」 -- [ ] 登录界面底部提供:「忘记用户名」链接、「忘记密码」链接(详见 Story 3、Story 4) - ---- - -### Story 3:经纪人找回用户名 - -**As** 忘记用户名的经纪人,**I want** 通过绑定的邮箱或手机号找回用户名,**So that** 不依赖管理员也能自助恢复登录。 - -**验收标准**: - -- [ ] 点击登录界面「忘记用户名」链接,跳转至「找回用户名」页面(或弹窗) -- [ ] 找回方式(本期以邮箱为主,手机号为预留字段): - - 邮箱找回:输入注册邮箱,系统校验邮箱是否与已知账号匹配,匹配成功则发送包含用户名的邮件至该邮箱 - - 手机号找回(预留,UI 入口以「即将开放」禁用态展示) -- [ ] 邮箱输入框:格式校验(包含「@」和域名),错误时提示「请输入有效的邮箱地址」 -- [ ] 点击「发送」后: - - 邮箱存在且已绑定账号 → 显示「用户名已发送至您的邮箱,请查收」;发送按钮进入 60 秒倒计时不可重复点击 - - 邮箱不存在 → **不提示「邮箱未注册」**(防止用户信息枚举),统一显示「如该邮箱已绑定账号,您将收到一封包含用户名的邮件」 -- [ ] 邮件内容:纯文本邮件,包含用户名、发送时间,及「如非本人操作请联系管理员」说明 -- [ ] 发送频率限制:同一邮箱 1 小时内最多发送 3 次 -- [ ] 提供「返回登录」链接 - ---- - -### Story 4:经纪人找回密码 - -**As** 忘记密码的经纪人,**I want** 通过已知用户名 + 绑定邮箱(或手机号)自助重置密码,**So that** 不依赖管理员也能快速恢复登录。 - -**验收标准**: - -- [ ] 点击登录界面「忘记密码」链接,跳转至「找回密码」流程(分步骤页面或 Stepper 组件) - -**步骤一:身份验证** - -- [ ] 用户输入:用户名 + 邮箱(本期);手机号找回为预留入口(禁用态) -- [ ] 服务端校验用户名与邮箱是否匹配,不泄露具体原因(统一提示「如信息匹配,重置链接将发送至您的邮箱」) -- [ ] 校验通过后,向绑定邮箱发送含一次性重置链接的邮件;链接有效期 **30 分钟**,使用后立即失效 -- [ ] 同一账号 1 小时内最多发送 3 次重置邮件 - -**步骤二:重置密码** - -- [ ] 用户点击邮件中的链接,跳转至「重置密码」页面(链接含加密 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 } -``` - -**绑定条件**(v2 实现时的前置要求): -- 手机号必须先在「个人设置」中与用户名账号完成绑定并通过验证 -- 一个手机号只能绑定一个用户名账号(同一租户内) -- 绑定手机号后,可通过手机号 + 短信验证码联合登录 - ---- - -### Story 6:预留——微信扫码登录(接口预留,v2 实现) - -**As** 绑定了微信账号的经纪人,**I want** 在登录界面扫描微信二维码完成登录,**So that** 免去输入账号密码的步骤,提升登录体验。 - -**当前状态**:本期 UI 入口以「即将开放」禁用态展示于登录界面,接口定义预留,不开放实际功能。 - -**预留接口设计**(供后端提前规划): - -``` -GET /api/auth/wechat/qrcode/ # 获取微信扫码二维码(含 state + 有效期) -POST /api/auth/wechat/callback/ # 微信扫码确认后回调,换取系统 Token -``` - -**绑定条件**(v2 实现时的前置要求): -- 微信账号必须先在「个人设置」中与用户名账号完成绑定 -- 二维码有效期 3 分钟,过期后前端自动刷新二维码 -- 微信账号只能绑定一个用户名账号(同一租户内) - ---- - -## 5. 功能详细说明 - -### 5.1 客户端 Tenant 识别流程 - -#### 5.1.1 流程概述 - -``` -客户端启动 - │ - ├─ 本地有 Tenant ID 缓存? - │ │ - │ YES ──→ 校验缓存 Tenant ID 是否仍有效(服务端 validate) - │ │ - │ 有效 ──→ 直接进入登录界面 - │ │ - │ 无效 ──→ 清除缓存,进入 Tenant 识别界面 - │ - └─ NO ──→ 显示 Tenant 识别界面 - │ - 用户输入 Tenant ID → 发起验证 - │ - 验证成功 ──→ 缓存 Tenant ID → 进入登录界面 - │ - 验证失败 ──→ 显示错误信息,保持识别界面 -``` - -#### 5.1.2 Tenant 识别界面规范 - -| 元素 | 规格 | -|------|------| -| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) | -| Logo | Fonrey 产品 Logo,居中显示 | -| 标题 | 「欢迎使用 Fonrey 房睿」 | -| 副标题 | 「请输入您公司的专属识别码以继续」 | -| Tenant ID 输入框 | 单行数字输入,固定 12 位,支持粘贴;非数字字符自动过滤,超出 12 位截断 | -| 输入框 Label | 「公司识别码(Tenant ID)」 | -| 确认按钮 | 主色调按钮,文字「确认」 | -| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) | -| 帮助文案 | 「不知道识别码?请联系您公司的系统管理员」 | - -#### 5.1.3 Tenant ID 格式规范 - -- **格式**:固定 **12 位纯数字**,如 `202500010001` -- **生成规则**(建议):由平台运营在系统管理后台开通租户时自动生成,不允许手动指定,确保全局唯一性;可采用时间戳前缀 + 随机后缀的方式生成(如 `YYYYMM` + 6 位随机数) -- **前端校验**:输入框仅接受数字字符(非数字自动过滤),输入满 12 位后自动触发格式完成状态;少于 12 位时点击「确认」弹出提示「识别码须为 12 位数字」 -- **唯一性**:全局唯一(公共 Schema 层面),同一 Tenant ID 不可分配给多个租户 -- **客户端存储**:Electron `app.getPath('userData')` 目录下的配置文件(加密存储,防止明文读取) - -#### 5.1.4 服务端 Tenant 验证接口规范 - -``` -POST /api/auth/tenant/verify/ - -Request Body: -{ - "tenant_id": "202500010001" -} - -Response 200 (成功): -{ - "valid": true, - "tenant_name": "XX房产经纪有限公司", - "tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png", - "login_url": "https://xxx.fonrey.com/auth/login/" -} - -Response 200 (失败): -{ - "valid": false, - "error_code": "TENANT_NOT_FOUND", - "message": "识别码无效" -} -``` - -> **注意**:该接口属于 `shared_apps` 范围,路由在公共 Schema 下,不需要租户鉴权,但需要限流保护(每 IP 每分钟 ≤ 10 次请求)。 - ---- - -### 5.2 登录界面设计规范 - -#### 5.2.1 界面布局 - -``` -┌─────────────────────────────────────────┐ -│ [租户 Logo] [租户公司名称] │ ← 顶部品牌区(Tenant 识别后回填) -│ │ -│ ┌───────────────────────┐ │ -│ │ 用户名 │ │ -│ └───────────────────────┘ │ -│ ┌───────────────────────┐ │ -│ │ 密码 👁 │ │ -│ └───────────────────────┘ │ -│ ┌─────────────────────────────────┐ │ -│ │ [背景图 + 拼图缺口] 🔄 │ │ ← 右上角刷新图标 -│ │ │ │ -│ │ [拼图碎片] │ │ -│ │ ├────────────────────────────── │ │ -│ │ ◀ 拖动滑块完成拼图 ▶ │ │ -│ └─────────────────────────────────┘ │ -│ ┌───────────────────────┐ │ -│ │ 登 录 │ │ ← 主 CTA,橙色 -│ └───────────────────────┘ │ -│ │ -│ 忘记用户名 忘记密码 │ ← 次级入口,文字链接 -│ │ -│ ─────────────── 其他登录 ──────────────│ -│ [手机验证码登录 - 即将开放] │ ← 禁用态,灰色 -│ [微信扫码登录 - 即将开放] │ ← 禁用态,灰色 -│ │ -│ 切换公司 │ ← 底部,小字链接 -└─────────────────────────────────────────┘ -``` - -#### 5.2.2 安全机制 - -| 机制 | 规格 | -|------|------| -| 验证码类型 | **滑块拼图行为验证码**:展示带缺口的背景图 + 可拖动的拼图碎片,用户滑动碎片至缺口完成验证,无需输入字符 | -| 验证逻辑 | 服务端综合校验**位置偏差**(缺口中心 ±5px 容差)+ **滑动轨迹特征**(加速/减速曲线、总耗时),双重判断是否为人类行为 | -| 背景图来源 | 预置图库随机抽取,缺口位置每次服务端随机生成,防止固定模式被预测 | -| 验证码有效期 | 单次验证会话有效,提交登录后服务端 Token 立即失效;超过 3 分钟未操作需重新加载 | -| 验证失败处理 | 拼图区域抖动动画提示,自动刷新新背景图;**不计入账号密码错误次数**(行为验证失败属独立事件) | -| 密码错误锁定 | 同一账号连续密码错误 ≥ 5 次,锁定 30 分钟;解锁方式:等待超时自动解锁 或 管理员手动解锁 | -| 密码错误计数 | 计数存于 Redis,Key 格式:`login_fail:tenant_id:username`,TTL 30 分钟 | -| 验证码刷新 | 登录失败(用户名/密码错误)后自动刷新拼图;用户亦可主动点击「刷新」图标重新加载背景图 | -| HTTPS | 所有登录相关请求强制 HTTPS,不允许 HTTP 降级 | -| 密码传输 | 前端不做密码加密,HTTPS 层保证传输安全;后端存储使用 `django.contrib.auth` 默认的 `PBKDF2+SHA256` 哈希 | -| Session 有效期 | 默认 8 小时(工作日单日使用场景);可由租户管理员在「系统设置」中调整 | - ---- - -### 5.3 账号与员工实名绑定规范 - -#### 5.3.1 绑定原则 - -- 每个系统登录账号必须与「组织人事管理」模块中的一条**员工档案(Staff)**绑定 -- 账号与员工是 **1:1 关系**,一个员工对应一个账号,一个账号只能绑定一个员工 -- **不支持用户自行注册**,所有账号均由有权限的管理角色创建 - -#### 5.3.2 账号创建权限分层 - -系统内共有两类账号创建场景,权限和规则各不相同: - -**① Tenant Admin 账号(每个租户唯一的超级管理账号)** - -| 项目 | 规格 | -|------|------| -| 创建时机 | 平台运营在系统管理后台开通租户时,同步创建第一个 Tenant Admin 账号 | -| 用户名 | **由平台运营自定义设置**,格式:英文字母开头,仅含字母/数字/下划线,6~30 字符,同租户内唯一 | -| 初始密码 | **由平台运营自定义设置**,须符合密码复杂度规则(8~32 位,含字母+数字) | -| 首次登录 | 强制修改初始密码,不可跳过 | -| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 | -| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号(后续可扩展为多管理员,v2 规划) | - -**② 普通员工账号(经纪人、店长、行政等)** - -| 项目 | 规格 | -|------|------| -| 创建时机 | Tenant Admin 在「组织人事管理 → 新增员工」时,系统自动为该员工创建登录账号 | -| 用户名 | **固定为该员工的手机号**(11 位数字),同租户内唯一,创建后不可更改 | -| 初始密码 | **系统统一固定初始密码**(由平台在部署配置中设定,如 `Fonrey@2025`),所有新员工账号均使用同一初始密码 | -| 首次登录 | 强制修改初始密码,**不可跳过**(详见 5.3.4) | -| 密码重置 | Tenant Admin 可在员工管理界面对任意员工账号执行「重置密码」,重置后恢复为固定初始密码,触发首次登录强制修改流程 | -| 账号禁用 | 员工离职或被停用时,对应账号自动禁用;禁用账号无法登录,历史操作记录保留 | - -#### 5.3.3 账号字段规范 - -| 字段 | 类型 | Tenant Admin | 普通员工账号 | 说明 | -|------|------|-------------|-------------|------| -| 用户名(username) | CharField(30) | 平台运营自定义,字母开头,含字母/数字/下划线,6~30 字符 | **固定为员工手机号**(11 位数字) | 登录 ID,创建后不可更改 | -| 密码(password) | CharField | 平台运营自定义初始密码 | **系统统一固定初始密码** | PBKDF2+SHA256 哈希存储 | -| 手机号(phone) | CharField(11) | 选填,加密存储 | **必填,同时作为用户名**,加密存储,同租户内唯一 | 当前阶段为登录 ID;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 时登录成功后强制跳转修改密码页 | -| 创建人(created_by) | ForeignKey → self | 平台运营(系统管理后台) | Tenant Admin | 审计追溯 | - -#### 5.3.4 首次登录强制修改密码 - -- 新员工账号创建后,`is_initial_password = True`,账号处于「初始密码」状态 -- 员工使用手机号(用户名)+ 固定初始密码登录成功后,系统**立即跳转**至「修改初始密码」强制页面,**不可关闭、不可跳过**,任何其他系统功能页面均不可访问 -- Tenant Admin 对员工账号执行「重置密码」后,`is_initial_password` 重置为 True,该员工下次登录时再次触发强制修改流程 -- 修改成功后,`is_initial_password` 更新为 False,原 Session 保持有效,直接进入系统首页 - -**强制修改密码页面规范**: - -| 元素 | 规格 | -|------|------| -| 页面标题 | 「欢迎使用 Fonrey,请先设置您的登录密码」 | -| 提示文案 | 「您当前使用的是初始密码,为保障账号安全,请立即设置新密码后开始使用」 | -| 新密码输入框 | 密文显示,右侧「显示/隐藏」切换 | -| 确认新密码输入框 | 密文显示,与新密码一致性实时校验 | -| 密码强度提示 | 逐条实时显示规则达标状态(✓/✗):长度 ≥ 8 位 / 包含字母 / 包含数字 | -| 提交按钮 | 「确认并进入系统」 | -| 不可操作项 | 无「跳过」按钮;顶部导航栏、侧边菜单、关闭按钮均禁用 | - ---- - -### 5.4 找回流程详细说明 - -#### 5.4.1 找回用户名流程 - -> **说明**:由于普通员工的用户名即为其**手机号**,通常无需「找回用户名」功能。登录界面的「忘记用户名」入口保留,但仅对 Tenant Admin 账号有意义(其用户名为自定义字符串)。 - -``` -用户点击「忘记用户名」 - │ - ├─ 普通员工:提示「您的登录账号为您的手机号,请直接使用手机号登录」 - │ 提供「返回登录」按钮 - │ - └─ Tenant Admin(用户名非手机号格式): - │ - ├─ 输入绑定邮箱 - │ │ - │ 服务端查询(不向前端返回查询结果,防止枚举) - │ │ - │ 统一响应「如该邮箱已绑定账号,您将收到邮件」 - │ │ - │ 后台:邮箱存在 → 发送邮件(包含用户名) - │ 邮箱不存在 → 静默处理 - │ - └─ 用户查收邮件,获取用户名 → 返回登录 -``` -![[找回用户名流程.png]] -> **前端识别逻辑**:用户在「忘记用户名」页面输入邮箱提交后,服务端根据是否匹配到 Tenant Admin 账号决定处理路径,前端无需区分,统一展示「如该邮箱已绑定账号,您将收到邮件」。 - -**邮件模板(找回用户名)**: - -``` -主题:您的 Fonrey 房睿用户名 - -您好, - -您请求找回在 [公司名称] 的 Fonrey 账号用户名。 - -您的用户名为:{username} - -如果这不是您的操作,请忽略此邮件。如有疑问,请联系您的系统管理员。 - -此邮件由系统自动发送,请勿回复。 -发送时间:{datetime} -``` - -#### 5.4.2 找回密码流程 - -``` -用户点击「忘记密码」 - │ -步骤1:身份验证 - │ - ├─ 输入手机号(即用户名)+ 绑定邮箱 - │ (Tenant Admin 则输入自定义用户名 + 绑定邮箱) - │ │ - │ 服务端校验用户名与邮箱是否匹配 - │ │ - │ 统一响应「如信息匹配,重置链接将发送至您的邮箱」(防止枚举) - │ │ - │ 后台:匹配成功 → 生成加密 Token(有效期 30min)→ 异步发送邮件 - │ 不匹配 → 静默处理 - │ -步骤2:用户点击邮件中的重置链接 - │ - ├─ 服务端校验 Token 有效性 - │ │ - │ 有效 → 展示「重置密码」表单 - │ │ - │ 无效/过期 → 提示「链接已过期,请重新申请」,提供「重新申请」按钮 - │ -步骤3:用户输入并提交新密码 - │ - ├─ 密码复杂度校验(≥ 8 位,含字母+数字) - ├─ 与历史密码对比校验(最近 3 次,含固定初始密码) - │ - └─ 校验通过 → 更新密码,is_initial_password = False - → 清除该账号所有有效 Session(强制重新登录) - → 跳转登录界面,提示「密码已重置,请重新登录」 -``` -![[找回密码流程.png]] -> **注意**:找回密码流程依赖员工账号绑定了邮箱。若员工未绑定邮箱,无法自助找回,需联系 Tenant Admin 在管理界面执行「重置密码」操作,将密码恢复为固定初始密码。 - -**邮件模板(重置密码)**: - -``` -主题:重置您的 Fonrey 房睿密码 - -您好, - -我们收到了重置您在 [公司名称] 的 Fonrey 账号密码的请求。 - -请点击以下链接重置密码(链接 30 分钟内有效): -{reset_link} - -如果您未发起此请求,请忽略此邮件,您的密码不会被更改。 - -此邮件由系统自动发送,请勿回复。 -发送时间:{datetime} -``` - ---- - -### 5.5 后端数据模型设计 - -> **数据模型已迁移至独立文档**,请参阅: -> **`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`** - -该文档包含: -- `user_accounts` 账号主表(完整字段定义、约束、索引、Django Model 代码) -- `login_attempts` 登录审计表 -- `password_reset_tokens` 密码重置令牌表 -- `password_histories` 历史密码记录表 -- Redis 缓存结构说明 -- 账号状态机与创建流程 -- 与 `org.Staff` 的关联规则及跨 App 依赖设计 -- Django Migrations 迁移顺序说明 -- 架构决策说明(ADR) - ---- - -### 5.6 Electron 客户端登录相关约定 - -| 约定项 | 规格 | -|--------|------| -| Tenant ID 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密,不存储明文 | -| Session Token 存储 | 内存(`global` 变量)+ `session` Cookie(Chromium 管理),不写入磁盘明文文件 | -| 登录页加载 | 客户端主进程根据 Tenant ID 构建目标 URL(`https://{tenant_slug}.fonrey.com/auth/login/`),通过 `BrowserWindow.loadURL()` 加载 | -| 多标签页处理 | 同一 `BrowserWindow` 内,所有页面共享同一 Session Cookie | -| 客户端登出 | 调用服务端 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie | -| 窗口关闭时 | Session 保留(不自动登出),下次打开客户端时若 Session 未过期,直接进入系统 | -| 强制更新场景 | 若客户端版本低于服务端 `min_required_version`,则在登录界面前先展示「请更新客户端」提示,阻断登录流程(参见发布管理模块 PRD)| - ---- - -## 6. 技术注意事项 - -### 6.1 依赖与技术选型 - -| 依赖项 | 用途 | 说明 | -|--------|------|------| -| `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` | 发送找回邮件 | 邮件发送异步处理,防止接口响应超时 | -| `django-ratelimit` 或自定义中间件 | 接口限流 | Tenant 验证接口、登录接口、找回密码接口均需限流 | -| `Pillow` | 滑块拼图图片处理 | 生成拼图背景图(抠出缺口区域)及对应的拼图碎片图片,输出为 Base64,分别通过两个字段返回给前端 | - -### 6.2 多租户下的 `UserAccount` 隔离 - -- `UserAccount` 表位于**租户 Schema 内**(`django-tenants` 租户隔离范围),因此 username 唯一性约束在租户维度生效,不同租户的经纪人可以有相同用户名 -- Tenant 验证接口(`/api/auth/tenant/verify/`)位于**公共 Schema**(`shared_apps`),使用 `TenantModel` 查询 -- 登录、找回密码等接口通过请求域名(`{tenant_slug}.fonrey.com`)切换到对应租户 Schema(`django-tenants` 中间件自动处理) - -### 6.3 已知风险 - -| 风险 | 可能性 | 影响 | 缓解措施 | -|------|--------|------|---------| -| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征(非线性运动特征),拒绝匀速/程序化轨迹;后续可引入设备指纹加固 | -| Tenant ID 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流(每IP每分钟≤10次),返回结果不区分「未找到」与「已禁用」| -| 密码重置 Token 泄露 | 低 | 高 | Token 单次有效、30分钟过期、HTTPS 传输 | -| 邮件发送失败导致用户无法找回密码 | 中 | 中 | 邮件发送失败写入告警日志,管理员可通过后台查看 Token 手动告知用户 | -| 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出,可在 Token 机制中引入版本号 | - -### 6.4 开放问题(开发前需确认) - -- [ ] **邮件服务商选型**:使用 SendGrid / 阿里云邮件推送 / SMTP 自建?需运维确认 — 负责人:后端负责人 — 截止:开发启动前 -- [ ] **Session 有效期默认值**:8 小时是否满足各租户需求?是否允许租户管理员自行配置?— 负责人:产品经理 — 截止:开发启动前 -- [ ] **滑块拼图实现方案**:自研(Pillow 生成图片 + 前端拖拽组件)还是集成第三方行为验证服务(如极验 GeeTest / 网易易盾)?自研可控但需维护图库;第三方开箱即用但引入外部依赖,需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前 -- [ ] **账号锁定通知**:账号被锁定后,是否自动发邮件通知用户和/或管理员?— 负责人:产品经理 — 截止:开发启动前 -- [ ] **历史密码校验范围**:最近 3 次是否足够?是否需要额外规则(如不能与用户名相同)?— 负责人:产品经理 — 截止:开发启动前 - ---- - -## 7. 发布计划 - -| 阶段 | 时间 | 受众 | 准入门槛 | -|------|------|------|---------| -| 内部 Alpha | 待定 | 研发团队 + 1 家种子租户 | 核心流程(Tenant 识别 + 账密登录 + 找回密码)无 P0 Bug | -| 封闭 Beta | 待定 | 5 ~ 10 家测试租户 | 登录成功率 ≥ 99%,验证码拦截机制正常运作 | -| 正式发布 | 待定 | 全量租户 | Beta 阶段无未修复的安全漏洞;帮助文档发布 | - -**回滚标准**:若正式发布后 24 小时内登录失败率(非验证码拦截原因)超过 2%,或出现账号数据泄露事件,立即回滚并启动安全审查。 - ---- - -## 8. 附录 - -### 8.1 登录状态流转图 - -``` -[未识别 Tenant] - │ 输入有效 Tenant ID - ↓ -[未登录] - │ 账密登录成功 - ↓ -[初始密码状态](如账号为初始密码) - │ 强制修改密码成功 - ↓ -[已登录 - Active Session] - │ Session 过期 / 主动登出 / 管理员强制登出 - ↓ -[未登录](跳转登录界面) - -[账号锁定状态](5次错误后) - │ 30 分钟后自动解锁 或 管理员手动解锁 - ↓ -[未登录](可重新登录) -``` - -### 8.2 接口清单汇总 - -| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 说明 | -|------|------|------------|------------|------| -| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | Tenant ID 验证 | -| `/api/auth/captcha/` | GET | Tenant | 否 | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token) | -| `/api/auth/captcha/verify/` | POST | Tenant | 否 | 提交滑动轨迹 + 位置,服务端校验并返回一次性通过凭证(供登录接口使用) | -| `/api/auth/login/` | POST | Tenant | 否 | 账号密码登录 | -| `/api/auth/logout/` | POST | Tenant | 是 | 登出,使 Session 失效 | -| `/api/auth/recover/username/` | POST | Tenant | 否 | 发起找回用户名 | -| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送邮件) | -| `/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 | 否 | **预留**,微信扫码回调 | - -### 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` +# PRD:用户登录管理模块 + +**状态**: Draft +**作者**: 产品经理 +**最后更新**: 2026-04-25(v1.4 §5.5 后端数据模型迁移至独立文档 `DATA_MODEL/DATA_MODEL_LOGIN.md`) +**版本**: 1.4 +**所属系统**: Fonrey 房产经纪管理系统 +**关联模块**: 组织人事管理、权限管理、系统管理 + +--- + +## 1. 问题陈述 + +### 1.1 背景 + +Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架构(`django-tenants` + PostgreSQL Schema 隔离)。终端用户通过 **Windows 桌面客户端(Electron)** 使用系统,无需手动输入网址即可打开 Web 应用。 + +在多租户环境下,用户的身份验证流程比单租户系统更复杂: + +- 用户安装客户端后,系统必须先识别当前设备归属哪个租户,才能加载对应租户的登录界面和数据隔离环境 +- 每家经纪公司作为独立租户,其员工账号、组织结构、数据均完全隔离 +- 经纪人账号须与实名员工档案绑定,确保每一条操作记录可追溯至具体自然人 +- 现阶段登录方式以账号密码为主;手机验证码登录、微信扫码登录需预留接口,待移动端(小程序)上线后实现 + +### 1.2 核心痛点 + +| 痛点 | 影响方 | 当前代价 | +|------|--------|---------| +| 多租户环境下,客户端不知道应该连接哪个租户的服务端 | 新用户首次安装后无法正常使用 | 系统无法启动,用户体验极差 | +| 账号密码裸露登录,缺乏验证码保护 | 所有用户 | 存在暴力破解、自动化恶意登录风险 | +| 用户忘记账号或密码无自助找回通道 | 一线经纪人 | 依赖管理员手动重置,效率低 | +| 账号未与实名经纪人档案绑定 | 系统管理员、合规审计 | 操作行为无法追溯至自然人 | +| 无多因素认证,安全系数低 | 管理层、数据合规 | 存在账号冒用、数据泄露风险 | + +### 1.3 目标用户 + +| 角色 | 描述 | 使用频率 | +|------|------|----------| +| 一线经纪人 | 每日登录系统使用房源/客源功能 | 每日高频 | +| 店长 / 经理 | 登录后查看全店数据、管理任务 | 每日 | +| 系统管理员 | 管理账号创建、密码重置、租户初始化 | 按需 | +| 新安装用户 | 首次安装客户端后需完成 Tenant 识别 | 一次性 | + +--- + +## 2. 目标与成功指标 + +| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 | +|------|------|----------|--------|----------| +| 降低登录失败率 | 账号密码正确情况下的登录成功率 | 待统计 | ≥ 99% | 上线后 30 天 | +| 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0(无保护) | 建立基线,同IP异常次数 > 5次/分钟触发封锁 | 上线后持续监控 | +| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约1工作日) | < 3 分钟(邮件/短信自助) | 上线后 30 天 | +| 确保账号实名绑定率 | 拥有系统账号且未与员工档案绑定的账号比例 | 待统计 | 0%(强制绑定) | 上线即达标 | +| Tenant 识别成功率 | 首次安装后成功完成 Tenant 识别的用户比例 | 待统计 | ≥ 98% | 上线后 30 天 | + +--- + +## 3. 非目标(本期不做) + +- **手机验证码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示 +- **微信扫码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示 +- **单点登录(SSO)/ 企业微信集成**:后续版本规划 +- **多设备并发登录的强制踢出策略**:本期允许同账号多端登录,后续安全策略模块规划 +- **登录时段限制 / IP 白名单**:安全策略模块另行规划 +- **管理后台(Platform Admin)登录**:系统管理员登录管理后台的流程属于系统管理模块,本 PRD 专注租户内用户登录 + +--- + +## 4. 用户故事与验收标准 + +--- + +### Story 1:新用户首次启动客户端——Tenant 识别 + +**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 Tenant ID 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。 + +**验收标准**: + +- [ ] 客户端首次启动时(本地无 Tenant ID 缓存),自动呈现「Tenant 识别」界面,而非直接显示登录界面 +- [ ] 界面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant ID 输入框、「确认」按钮 +- [ ] Tenant ID 输入框支持粘贴操作,自动去除前后空格 +- [ ] 点击「确认」后,客户端向服务端发起 Tenant 验证请求(`POST /api/auth/tenant/verify/`),展示加载状态(spinner) +- [ ] **验证成功**:服务端返回租户名称及品牌信息(如公司名称、Logo URL);客户端将 Tenant ID 写入本地持久化存储,自动跳转至该租户的登录界面;界面顶部展示「正在登录:XX 房产」 +- [ ] **验证失败(Tenant ID 无效)**:输入框下方显示红色错误提示「识别码无效,请联系您的系统管理员获取正确的识别码」;Tenant ID 不写入本地缓存;用户可重新输入 +- [ ] **网络异常**:显示「网络连接失败,请检查网络后重试」,提供「重试」按钮 +- [ ] 非首次启动(本地已有合法 Tenant ID 缓存):直接跳过识别界面,进入登录界面 +- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA),点击后清除本地 Tenant ID 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」 +- [ ] Tenant 验证接口属于公开接口,无需鉴权;但需对单 IP 请求频率限制(每分钟 ≤ 10 次)以防止枚举攻击 + +--- + +### Story 2:经纪人通过账号密码登录 + +**As** 已识别租户的经纪人,**I want** 通过用户名和密码完成登录,**So that** 进入系统开始工作。 + +**验收标准**: + +- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、用户名输入框、密码输入框、滑块拼图验证区域、「登录」按钮 +- [ ] 用户名输入框 Placeholder:「请输入用户名」;支持英文字母、数字、下划线,最大长度 50 字符 +- [ ] 密码输入框默认密文显示,右侧提供「显示/隐藏」图标切换明密文 +- [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符,操作直观快速 +- [ ] 验证逻辑:前端记录滑动轨迹(坐标序列 + 耗时),与背景图缺口位置一同发送至服务端;服务端综合校验**位置偏差**(允许 ±5px 容差)和**轨迹特征**(是否存在人类滑动的加速/减速规律)以区分机器行为 +- [ ] 验证失败(位置不准或轨迹异常):拼图区域抖动动画提示失败,自动刷新新的背景图,用户重新拖动;**不计入账号密码错误次数** +- [ ] 验证成功后,拼图区域显示绿色对勾 + 「验证通过」文案,状态持续至本次登录提交完成 +- [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图(针对图片模糊或缺口不清晰的情况) +- [ ] 背景图从预置图库中随机抽取,缺口位置每次随机生成,防止固定模式被预测 +- [ ] 三项(用户名、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰) +- [ ] 点击「登录」触发前端格式校验: + - 用户名为空 → 输入框下方红色提示「请输入用户名」 + - 密码为空 → 提示「请输入密码」 + - 验证码为空 → 提示「请输入验证码」 +- [ ] 格式校验通过后,向服务端发起登录请求,按钮进入 loading 状态防止重复提交 +- [ ] **登录成功**:服务端返回 Session Token;客户端存储 Token;跳转至系统首页;顶部显示欢迎信息「欢迎回来,{姓名}」 +- [ ] **登录失败(用户名或密码错误)**:显示「用户名或密码错误,请重新输入」(不区分是用户名错误还是密码错误,防止枚举攻击);验证码自动刷新;密码输入框清空;用户名保留 +- [ ] **登录失败(验证码错误)**:显示「验证码有误,请重新输入」;验证码自动刷新;验证码输入框清空 +- [ ] **账号被锁定**(同一账号密码连续错误 ≥ 5 次):显示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」;锁定状态下「登录」按钮置灰 +- [ ] **账号已停用**:显示「账号已停用,请联系您的管理员」 +- [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录界面,并提示「登录已过期,请重新登录」 +- [ ] 登录界面底部提供:「忘记用户名」链接、「忘记密码」链接(详见 Story 3、Story 4) + +--- + +### Story 3:经纪人找回用户名 + +**As** 忘记用户名的经纪人,**I want** 通过绑定的邮箱或手机号找回用户名,**So that** 不依赖管理员也能自助恢复登录。 + +**验收标准**: + +- [ ] 点击登录界面「忘记用户名」链接,跳转至「找回用户名」页面(或弹窗) +- [ ] 找回方式(本期以邮箱为主,手机号为预留字段): + - 邮箱找回:输入注册邮箱,系统校验邮箱是否与已知账号匹配,匹配成功则发送包含用户名的邮件至该邮箱 + - 手机号找回(预留,UI 入口以「即将开放」禁用态展示) +- [ ] 邮箱输入框:格式校验(包含「@」和域名),错误时提示「请输入有效的邮箱地址」 +- [ ] 点击「发送」后: + - 邮箱存在且已绑定账号 → 显示「用户名已发送至您的邮箱,请查收」;发送按钮进入 60 秒倒计时不可重复点击 + - 邮箱不存在 → **不提示「邮箱未注册」**(防止用户信息枚举),统一显示「如该邮箱已绑定账号,您将收到一封包含用户名的邮件」 +- [ ] 邮件内容:纯文本邮件,包含用户名、发送时间,及「如非本人操作请联系管理员」说明 +- [ ] 发送频率限制:同一邮箱 1 小时内最多发送 3 次 +- [ ] 提供「返回登录」链接 + +--- + +### Story 4:经纪人找回密码 + +**As** 忘记密码的经纪人,**I want** 通过已知用户名 + 绑定邮箱(或手机号)自助重置密码,**So that** 不依赖管理员也能快速恢复登录。 + +**验收标准**: + +- [ ] 点击登录界面「忘记密码」链接,跳转至「找回密码」流程(分步骤页面或 Stepper 组件) + +**步骤一:身份验证** + +- [ ] 用户输入:用户名 + 邮箱(本期);手机号找回为预留入口(禁用态) +- [ ] 服务端校验用户名与邮箱是否匹配,不泄露具体原因(统一提示「如信息匹配,重置链接将发送至您的邮箱」) +- [ ] 校验通过后,向绑定邮箱发送含一次性重置链接的邮件;链接有效期 **30 分钟**,使用后立即失效 +- [ ] 同一账号 1 小时内最多发送 3 次重置邮件 + +**步骤二:重置密码** + +- [ ] 用户点击邮件中的链接,跳转至「重置密码」页面(链接含加密 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 } +``` + +**绑定条件**(v2 实现时的前置要求): +- 手机号必须先在「个人设置」中与用户名账号完成绑定并通过验证 +- 一个手机号只能绑定一个用户名账号(同一租户内) +- 绑定手机号后,可通过手机号 + 短信验证码联合登录 + +--- + +### Story 6:预留——微信扫码登录(接口预留,v2 实现) + +**As** 绑定了微信账号的经纪人,**I want** 在登录界面扫描微信二维码完成登录,**So that** 免去输入账号密码的步骤,提升登录体验。 + +**当前状态**:本期 UI 入口以「即将开放」禁用态展示于登录界面,接口定义预留,不开放实际功能。 + +**预留接口设计**(供后端提前规划): + +``` +GET /api/auth/wechat/qrcode/ # 获取微信扫码二维码(含 state + 有效期) +POST /api/auth/wechat/callback/ # 微信扫码确认后回调,换取系统 Token +``` + +**绑定条件**(v2 实现时的前置要求): +- 微信账号必须先在「个人设置」中与用户名账号完成绑定 +- 二维码有效期 3 分钟,过期后前端自动刷新二维码 +- 微信账号只能绑定一个用户名账号(同一租户内) + +--- + +## 5. 功能详细说明 + +### 5.1 客户端 Tenant 识别流程 + +#### 5.1.1 流程概述 + +``` +客户端启动 + │ + ├─ 本地有 Tenant ID 缓存? + │ │ + │ YES ──→ 校验缓存 Tenant ID 是否仍有效(服务端 validate) + │ │ + │ 有效 ──→ 直接进入登录界面 + │ │ + │ 无效 ──→ 清除缓存,进入 Tenant 识别界面 + │ + └─ NO ──→ 显示 Tenant 识别界面 + │ + 用户输入 Tenant ID → 发起验证 + │ + 验证成功 ──→ 缓存 Tenant ID → 进入登录界面 + │ + 验证失败 ──→ 显示错误信息,保持识别界面 +``` + +#### 5.1.2 Tenant 识别界面规范 + +| 元素 | 规格 | +|------|------| +| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) | +| Logo | Fonrey 产品 Logo,居中显示 | +| 标题 | 「欢迎使用 Fonrey 房睿」 | +| 副标题 | 「请输入您公司的专属识别码以继续」 | +| Tenant ID 输入框 | 单行数字输入,固定 12 位,支持粘贴;非数字字符自动过滤,超出 12 位截断 | +| 输入框 Label | 「公司识别码(Tenant ID)」 | +| 确认按钮 | 主色调按钮,文字「确认」 | +| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) | +| 帮助文案 | 「不知道识别码?请联系您公司的系统管理员」 | + +#### 5.1.3 Tenant ID 格式规范 + +- **格式**:固定 **12 位纯数字**,如 `202500010001` +- **生成规则**(建议):由平台运营在系统管理后台开通租户时自动生成,不允许手动指定,确保全局唯一性;可采用时间戳前缀 + 随机后缀的方式生成(如 `YYYYMM` + 6 位随机数) +- **前端校验**:输入框仅接受数字字符(非数字自动过滤),输入满 12 位后自动触发格式完成状态;少于 12 位时点击「确认」弹出提示「识别码须为 12 位数字」 +- **唯一性**:全局唯一(公共 Schema 层面),同一 Tenant ID 不可分配给多个租户 +- **客户端存储**:Electron `app.getPath('userData')` 目录下的配置文件(加密存储,防止明文读取) + +#### 5.1.4 服务端 Tenant 验证接口规范 + +``` +POST /api/auth/tenant/verify/ + +Request Body: +{ + "tenant_id": "202500010001" +} + +Response 200 (成功): +{ + "valid": true, + "tenant_name": "XX房产经纪有限公司", + "tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png", + "login_url": "https://xxx.fonrey.com/auth/login/" +} + +Response 200 (失败): +{ + "valid": false, + "error_code": "TENANT_NOT_FOUND", + "message": "识别码无效" +} +``` + +> **注意**:该接口属于 `shared_apps` 范围,路由在公共 Schema 下,不需要租户鉴权,但需要限流保护(每 IP 每分钟 ≤ 10 次请求)。 + +--- + +### 5.2 登录界面设计规范 + +#### 5.2.1 界面布局 + +``` +┌─────────────────────────────────────────┐ +│ [租户 Logo] [租户公司名称] │ ← 顶部品牌区(Tenant 识别后回填) +│ │ +│ ┌───────────────────────┐ │ +│ │ 用户名 │ │ +│ └───────────────────────┘ │ +│ ┌───────────────────────┐ │ +│ │ 密码 👁 │ │ +│ └───────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ [背景图 + 拼图缺口] 🔄 │ │ ← 右上角刷新图标 +│ │ │ │ +│ │ [拼图碎片] │ │ +│ │ ├────────────────────────────── │ │ +│ │ ◀ 拖动滑块完成拼图 ▶ │ │ +│ └─────────────────────────────────┘ │ +│ ┌───────────────────────┐ │ +│ │ 登 录 │ │ ← 主 CTA,橙色 +│ └───────────────────────┘ │ +│ │ +│ 忘记用户名 忘记密码 │ ← 次级入口,文字链接 +│ │ +│ ─────────────── 其他登录 ──────────────│ +│ [手机验证码登录 - 即将开放] │ ← 禁用态,灰色 +│ [微信扫码登录 - 即将开放] │ ← 禁用态,灰色 +│ │ +│ 切换公司 │ ← 底部,小字链接 +└─────────────────────────────────────────┘ +``` + +#### 5.2.2 安全机制 + +| 机制 | 规格 | +|------|------| +| 验证码类型 | **滑块拼图行为验证码**:展示带缺口的背景图 + 可拖动的拼图碎片,用户滑动碎片至缺口完成验证,无需输入字符 | +| 验证逻辑 | 服务端综合校验**位置偏差**(缺口中心 ±5px 容差)+ **滑动轨迹特征**(加速/减速曲线、总耗时),双重判断是否为人类行为 | +| 背景图来源 | 预置图库随机抽取,缺口位置每次服务端随机生成,防止固定模式被预测 | +| 验证码有效期 | 单次验证会话有效,提交登录后服务端 Token 立即失效;超过 3 分钟未操作需重新加载 | +| 验证失败处理 | 拼图区域抖动动画提示,自动刷新新背景图;**不计入账号密码错误次数**(行为验证失败属独立事件) | +| 密码错误锁定 | 同一账号连续密码错误 ≥ 5 次,锁定 30 分钟;解锁方式:等待超时自动解锁 或 管理员手动解锁 | +| 密码错误计数 | 计数存于 Redis,Key 格式:`login_fail:tenant_id:username`,TTL 30 分钟 | +| 验证码刷新 | 登录失败(用户名/密码错误)后自动刷新拼图;用户亦可主动点击「刷新」图标重新加载背景图 | +| HTTPS | 所有登录相关请求强制 HTTPS,不允许 HTTP 降级 | +| 密码传输 | 前端不做密码加密,HTTPS 层保证传输安全;后端存储使用 `django.contrib.auth` 默认的 `PBKDF2+SHA256` 哈希 | +| Session 有效期 | 默认 8 小时(工作日单日使用场景);可由租户管理员在「系统设置」中调整 | + +--- + +### 5.3 账号与员工实名绑定规范 + +#### 5.3.1 绑定原则 + +- 每个系统登录账号必须与「组织人事管理」模块中的一条**员工档案(Staff)**绑定 +- 账号与员工是 **1:1 关系**,一个员工对应一个账号,一个账号只能绑定一个员工 +- **不支持用户自行注册**,所有账号均由有权限的管理角色创建 + +#### 5.3.2 账号创建权限分层 + +系统内共有两类账号创建场景,权限和规则各不相同: + +**① Tenant Admin 账号(每个租户唯一的超级管理账号)** + +| 项目 | 规格 | +|------|------| +| 创建时机 | 平台运营在系统管理后台开通租户时,同步创建第一个 Tenant Admin 账号 | +| 用户名 | **由平台运营自定义设置**,格式:英文字母开头,仅含字母/数字/下划线,6~30 字符,同租户内唯一 | +| 初始密码 | **由平台运营自定义设置**,须符合密码复杂度规则(8~32 位,含字母+数字) | +| 首次登录 | 强制修改初始密码,不可跳过 | +| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 | +| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号(后续可扩展为多管理员,v2 规划) | + +**② 普通员工账号(经纪人、店长、行政等)** + +| 项目 | 规格 | +|------|------| +| 创建时机 | Tenant Admin 在「组织人事管理 → 新增员工」时,系统自动为该员工创建登录账号 | +| 用户名 | **固定为该员工的手机号**(11 位数字),同租户内唯一,创建后不可更改 | +| 初始密码 | **系统统一固定初始密码**(由平台在部署配置中设定,如 `Fonrey@2025`),所有新员工账号均使用同一初始密码 | +| 首次登录 | 强制修改初始密码,**不可跳过**(详见 5.3.4) | +| 密码重置 | Tenant Admin 可在员工管理界面对任意员工账号执行「重置密码」,重置后恢复为固定初始密码,触发首次登录强制修改流程 | +| 账号禁用 | 员工离职或被停用时,对应账号自动禁用;禁用账号无法登录,历史操作记录保留 | + +#### 5.3.3 账号字段规范 + +| 字段 | 类型 | Tenant Admin | 普通员工账号 | 说明 | +|------|------|-------------|-------------|------| +| 用户名(username) | CharField(30) | 平台运营自定义,字母开头,含字母/数字/下划线,6~30 字符 | **固定为员工手机号**(11 位数字) | 登录 ID,创建后不可更改 | +| 密码(password) | CharField | 平台运营自定义初始密码 | **系统统一固定初始密码** | PBKDF2+SHA256 哈希存储 | +| 手机号(phone) | CharField(11) | 选填,加密存储 | **必填,同时作为用户名**,加密存储,同租户内唯一 | 当前阶段为登录 ID;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 时登录成功后强制跳转修改密码页 | +| 创建人(created_by) | ForeignKey → self | 平台运营(系统管理后台) | Tenant Admin | 审计追溯 | + +#### 5.3.4 首次登录强制修改密码 + +- 新员工账号创建后,`is_initial_password = True`,账号处于「初始密码」状态 +- 员工使用手机号(用户名)+ 固定初始密码登录成功后,系统**立即跳转**至「修改初始密码」强制页面,**不可关闭、不可跳过**,任何其他系统功能页面均不可访问 +- Tenant Admin 对员工账号执行「重置密码」后,`is_initial_password` 重置为 True,该员工下次登录时再次触发强制修改流程 +- 修改成功后,`is_initial_password` 更新为 False,原 Session 保持有效,直接进入系统首页 + +**强制修改密码页面规范**: + +| 元素 | 规格 | +|------|------| +| 页面标题 | 「欢迎使用 Fonrey,请先设置您的登录密码」 | +| 提示文案 | 「您当前使用的是初始密码,为保障账号安全,请立即设置新密码后开始使用」 | +| 新密码输入框 | 密文显示,右侧「显示/隐藏」切换 | +| 确认新密码输入框 | 密文显示,与新密码一致性实时校验 | +| 密码强度提示 | 逐条实时显示规则达标状态(✓/✗):长度 ≥ 8 位 / 包含字母 / 包含数字 | +| 提交按钮 | 「确认并进入系统」 | +| 不可操作项 | 无「跳过」按钮;顶部导航栏、侧边菜单、关闭按钮均禁用 | + +--- + +### 5.4 找回流程详细说明 + +#### 5.4.1 找回用户名流程 + +> **说明**:由于普通员工的用户名即为其**手机号**,通常无需「找回用户名」功能。登录界面的「忘记用户名」入口保留,但仅对 Tenant Admin 账号有意义(其用户名为自定义字符串)。 + +``` +用户点击「忘记用户名」 + │ + ├─ 普通员工:提示「您的登录账号为您的手机号,请直接使用手机号登录」 + │ 提供「返回登录」按钮 + │ + └─ Tenant Admin(用户名非手机号格式): + │ + ├─ 输入绑定邮箱 + │ │ + │ 服务端查询(不向前端返回查询结果,防止枚举) + │ │ + │ 统一响应「如该邮箱已绑定账号,您将收到邮件」 + │ │ + │ 后台:邮箱存在 → 发送邮件(包含用户名) + │ 邮箱不存在 → 静默处理 + │ + └─ 用户查收邮件,获取用户名 → 返回登录 +``` +![[找回用户名流程.png]] +> **前端识别逻辑**:用户在「忘记用户名」页面输入邮箱提交后,服务端根据是否匹配到 Tenant Admin 账号决定处理路径,前端无需区分,统一展示「如该邮箱已绑定账号,您将收到邮件」。 + +**邮件模板(找回用户名)**: + +``` +主题:您的 Fonrey 房睿用户名 + +您好, + +您请求找回在 [公司名称] 的 Fonrey 账号用户名。 + +您的用户名为:{username} + +如果这不是您的操作,请忽略此邮件。如有疑问,请联系您的系统管理员。 + +此邮件由系统自动发送,请勿回复。 +发送时间:{datetime} +``` + +#### 5.4.2 找回密码流程 + +``` +用户点击「忘记密码」 + │ +步骤1:身份验证 + │ + ├─ 输入手机号(即用户名)+ 绑定邮箱 + │ (Tenant Admin 则输入自定义用户名 + 绑定邮箱) + │ │ + │ 服务端校验用户名与邮箱是否匹配 + │ │ + │ 统一响应「如信息匹配,重置链接将发送至您的邮箱」(防止枚举) + │ │ + │ 后台:匹配成功 → 生成加密 Token(有效期 30min)→ 异步发送邮件 + │ 不匹配 → 静默处理 + │ +步骤2:用户点击邮件中的重置链接 + │ + ├─ 服务端校验 Token 有效性 + │ │ + │ 有效 → 展示「重置密码」表单 + │ │ + │ 无效/过期 → 提示「链接已过期,请重新申请」,提供「重新申请」按钮 + │ +步骤3:用户输入并提交新密码 + │ + ├─ 密码复杂度校验(≥ 8 位,含字母+数字) + ├─ 与历史密码对比校验(最近 3 次,含固定初始密码) + │ + └─ 校验通过 → 更新密码,is_initial_password = False + → 清除该账号所有有效 Session(强制重新登录) + → 跳转登录界面,提示「密码已重置,请重新登录」 +``` +![[找回密码流程.png]] +> **注意**:找回密码流程依赖员工账号绑定了邮箱。若员工未绑定邮箱,无法自助找回,需联系 Tenant Admin 在管理界面执行「重置密码」操作,将密码恢复为固定初始密码。 + +**邮件模板(重置密码)**: + +``` +主题:重置您的 Fonrey 房睿密码 + +您好, + +我们收到了重置您在 [公司名称] 的 Fonrey 账号密码的请求。 + +请点击以下链接重置密码(链接 30 分钟内有效): +{reset_link} + +如果您未发起此请求,请忽略此邮件,您的密码不会被更改。 + +此邮件由系统自动发送,请勿回复。 +发送时间:{datetime} +``` + +--- + +### 5.5 后端数据模型设计 + +> **数据模型已迁移至独立文档**,请参阅: +> **`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`** + +该文档包含: +- `user_accounts` 账号主表(完整字段定义、约束、索引、Django Model 代码) +- `login_attempts` 登录审计表 +- `password_reset_tokens` 密码重置令牌表 +- `password_histories` 历史密码记录表 +- Redis 缓存结构说明 +- 账号状态机与创建流程 +- 与 `org.Staff` 的关联规则及跨 App 依赖设计 +- Django Migrations 迁移顺序说明 +- 架构决策说明(ADR) + +--- + +### 5.6 Electron 客户端登录相关约定 + +| 约定项 | 规格 | +|--------|------| +| Tenant ID 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密,不存储明文 | +| Session Token 存储 | 内存(`global` 变量)+ `session` Cookie(Chromium 管理),不写入磁盘明文文件 | +| 登录页加载 | 客户端主进程根据 Tenant ID 构建目标 URL(`https://{tenant_slug}.fonrey.com/auth/login/`),通过 `BrowserWindow.loadURL()` 加载 | +| 多标签页处理 | 同一 `BrowserWindow` 内,所有页面共享同一 Session Cookie | +| 客户端登出 | 调用服务端 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie | +| 窗口关闭时 | Session 保留(不自动登出),下次打开客户端时若 Session 未过期,直接进入系统 | +| 强制更新场景 | 若客户端版本低于服务端 `min_required_version`,则在登录界面前先展示「请更新客户端」提示,阻断登录流程(参见发布管理模块 PRD)| + +--- + +## 6. 技术注意事项 + +### 6.1 依赖与技术选型 + +| 依赖项 | 用途 | 说明 | +|--------|------|------| +| `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` | 发送找回邮件 | 邮件发送异步处理,防止接口响应超时 | +| `django-ratelimit` 或自定义中间件 | 接口限流 | Tenant 验证接口、登录接口、找回密码接口均需限流 | +| `Pillow` | 滑块拼图图片处理 | 生成拼图背景图(抠出缺口区域)及对应的拼图碎片图片,输出为 Base64,分别通过两个字段返回给前端 | + +### 6.2 多租户下的 `UserAccount` 隔离 + +- `UserAccount` 表位于**租户 Schema 内**(`django-tenants` 租户隔离范围),因此 username 唯一性约束在租户维度生效,不同租户的经纪人可以有相同用户名 +- Tenant 验证接口(`/api/auth/tenant/verify/`)位于**公共 Schema**(`shared_apps`),使用 `TenantModel` 查询 +- 登录、找回密码等接口通过请求域名(`{tenant_slug}.fonrey.com`)切换到对应租户 Schema(`django-tenants` 中间件自动处理) + +### 6.3 已知风险 + +| 风险 | 可能性 | 影响 | 缓解措施 | +|------|--------|------|---------| +| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征(非线性运动特征),拒绝匀速/程序化轨迹;后续可引入设备指纹加固 | +| Tenant ID 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流(每IP每分钟≤10次),返回结果不区分「未找到」与「已禁用」| +| 密码重置 Token 泄露 | 低 | 高 | Token 单次有效、30分钟过期、HTTPS 传输 | +| 邮件发送失败导致用户无法找回密码 | 中 | 中 | 邮件发送失败写入告警日志,管理员可通过后台查看 Token 手动告知用户 | +| 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出,可在 Token 机制中引入版本号 | + +### 6.4 开放问题(开发前需确认) + +- [ ] **邮件服务商选型**:使用 SendGrid / 阿里云邮件推送 / SMTP 自建?需运维确认 — 负责人:后端负责人 — 截止:开发启动前 +- [ ] **Session 有效期默认值**:8 小时是否满足各租户需求?是否允许租户管理员自行配置?— 负责人:产品经理 — 截止:开发启动前 +- [ ] **滑块拼图实现方案**:自研(Pillow 生成图片 + 前端拖拽组件)还是集成第三方行为验证服务(如极验 GeeTest / 网易易盾)?自研可控但需维护图库;第三方开箱即用但引入外部依赖,需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前 +- [ ] **账号锁定通知**:账号被锁定后,是否自动发邮件通知用户和/或管理员?— 负责人:产品经理 — 截止:开发启动前 +- [ ] **历史密码校验范围**:最近 3 次是否足够?是否需要额外规则(如不能与用户名相同)?— 负责人:产品经理 — 截止:开发启动前 + +--- + +## 7. 发布计划 + +| 阶段 | 时间 | 受众 | 准入门槛 | +|------|------|------|---------| +| 内部 Alpha | 待定 | 研发团队 + 1 家种子租户 | 核心流程(Tenant 识别 + 账密登录 + 找回密码)无 P0 Bug | +| 封闭 Beta | 待定 | 5 ~ 10 家测试租户 | 登录成功率 ≥ 99%,验证码拦截机制正常运作 | +| 正式发布 | 待定 | 全量租户 | Beta 阶段无未修复的安全漏洞;帮助文档发布 | + +**回滚标准**:若正式发布后 24 小时内登录失败率(非验证码拦截原因)超过 2%,或出现账号数据泄露事件,立即回滚并启动安全审查。 + +--- + +## 8. 附录 + +### 8.1 登录状态流转图 + +``` +[未识别 Tenant] + │ 输入有效 Tenant ID + ↓ +[未登录] + │ 账密登录成功 + ↓ +[初始密码状态](如账号为初始密码) + │ 强制修改密码成功 + ↓ +[已登录 - Active Session] + │ Session 过期 / 主动登出 / 管理员强制登出 + ↓ +[未登录](跳转登录界面) + +[账号锁定状态](5次错误后) + │ 30 分钟后自动解锁 或 管理员手动解锁 + ↓ +[未登录](可重新登录) +``` + +### 8.2 接口清单汇总 + +| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 说明 | +|------|------|------------|------------|------| +| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | Tenant ID 验证 | +| `/api/auth/captcha/` | GET | Tenant | 否 | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token) | +| `/api/auth/captcha/verify/` | POST | Tenant | 否 | 提交滑动轨迹 + 位置,服务端校验并返回一次性通过凭证(供登录接口使用) | +| `/api/auth/login/` | POST | Tenant | 否 | 账号密码登录 | +| `/api/auth/logout/` | POST | Tenant | 是 | 登出,使 Session 失效 | +| `/api/auth/recover/username/` | POST | Tenant | 否 | 发起找回用户名 | +| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送邮件) | +| `/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 | 否 | **预留**,微信扫码回调 | + +### 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/TECH_STACK/TECH_STACK.md b/Project/fonrey/TECH_STACK/TECH_STACK.md index 08d345e1..8886cca2 100644 --- a/Project/fonrey/TECH_STACK/TECH_STACK.md +++ b/Project/fonrey/TECH_STACK/TECH_STACK.md @@ -54,15 +54,15 @@ ``` fonrey/ ├── apps/ -│ ├── tenants/ # django-tenants 配置(shared_apps) -│ ├── accounts/ # 登录认证(详见 登录管理技术方案.md) -│ ├── permissions/ # 权限管理(详见 权限管理系统技术方案.md) +│ ├── tenant/ # django-tenants 配置(shared_apps) +│ ├── account/ # 登录认证(详见 登录管理技术方案.md) +│ ├── permission/ # 权限管理(详见 权限管理系统技术方案.md) │ ├── org/ # 组织人事(org_units, staff) │ ├── region/ # 区域管理(districts, business_areas, metro) │ ├── complex/ # 楼盘管理(complexes, buildings, schools) │ ├── property/ # 房源核心(含 models/services/tasks 三层) │ ├── client/ # 客源管理 -│ ├── settings/ # 系统设置(lookup, tags) +│ ├── setting/ # 系统设置(lookup, tags) │ └── release/ # 客户端发布管理(shared_apps) ├── shared/ # 公共 Schema App └── core/ @@ -144,10 +144,51 @@ apps/property/ --- -## 9. 文档维护原则 +## 9. 测试策略 + +> **完整测试规范**见:[`测试规范.md`](./测试规范.md)。本节仅列关键结论。 + +Fonrey 采用 AI vibe coding 模式开发,测试是保证每日迭代质量的唯一安全网。**每个 P0 User Story 完成后,对应测试必须同步产出,不允许欠测试债。** + +### 测试分层 + +| 层级 | 工具 | 覆盖目标 | 运行频率 | +|------|------|---------|---------| +| **单元测试** | `pytest-django` + `factory_boy` | `core/`、`services/`、`tasks.py` | 每次 push | +| **集成测试** | `pytest-django` TenantClient | 所有 P0 User Story 的 HTTP 接口 | 每次 push | +| **E2E 测试** | `playwright` (Python) | 5 条核心用户旅程 | 每日定时 | + +### 关键约定 + +- 所有集成测试必须使用 `django-tenants` 的 `TenantClient`,禁止使用 Django 原生 `Client()` +- HTMX 局部请求测试须携带 `HTTP_HX_REQUEST: true` header,并验证返回局部 HTML 而非完整页面 +- Celery 任务测试使用 `CELERY_TASK_ALWAYS_EAGER = True` 同步执行 +- 外部服务(R2、Redis、邮件)在测试中全部 Mock,禁止真实调用 +- 每个受权限保护的 View,必须覆盖:有权限(200)、无权限(403)、未登录(302)三个场景 + +### 覆盖率基准 + +| 模块 | 最低目标 | +|------|---------| +| `core/` 核心基础模块 | ≥ 90% | +| `apps/*/services/` 业务逻辑层 | ≥ 80% | +| `apps/*/views.py` 视图层 | ≥ 70% | +| E2E 核心用户旅程(5 条) | 100% 通过 | + +### CI 自动化 + +- 每次 push 到 `main` / `develop` 自动运行单元测试 + 集成测试 +- 每日北京时间凌晨 2 点自动运行全量套件(含 E2E) +- 配置文件:`.github/workflows/daily-test.yml` + +--- + +## 10. 文档维护原则 - 本文档仅记录**跨模块共识**与**模块索引**,不展开模块细节 - 模块技术方案在子文档中维护,并通过 §8 表格回链 - 任何技术栈变更(替换组件、升级主版本、新增外部服务)须同步更新本文档 §2、§5、§6 - 新增模块时,先在 §4 目录结构补位,再在 §8 索引登记子文档 +- 测试规范变更须同步更新 §9 关键结论,完整细节在 [`测试规范.md`](./测试规范.md) 中维护 + diff --git a/Project/fonrey/TECH_STACK/测试规范.md b/Project/fonrey/TECH_STACK/测试规范.md new file mode 100644 index 00000000..0d0efba3 --- /dev/null +++ b/Project/fonrey/TECH_STACK/测试规范.md @@ -0,0 +1,611 @@ +# Fonrey 测试规范(TEST_SPEC) + +> **For AI assistants**: Read this entire file before writing any test code. All decisions here are final. Do not suggest alternatives unless asked. Every new feature or User Story implementation must be accompanied by corresponding tests as defined in this document. + +**版本**: 1.0 | **最后更新**: 2026-04-26 +**定位**: 本文档定义 Fonrey 项目的完整测试策略,包含测试分层、工具选型、目录结构、多租户测试约定、HTMX 测试约定、CI 自动化配置及 AI 辅助编码时的测试要求。 + +--- + +## 1. 测试目标 + +Fonrey 采用 AI vibe coding 模式开发,AI 负责生成功能代码,**测试是保证每日迭代质量的唯一安全网**。测试体系须满足: + +- 每个 P0 User Story 完成后,对应测试同步产出 +- 每日自动运行全量测试套件,输出可读报告 +- 测试失败时,AI 可根据报告自主定位并修复问题 +- 测试环境与生产环境技术栈完全一致(同样使用 PostgreSQL + django-tenants) + +**覆盖率基准目标**: + +| 层级 | 最低目标 | +|------|---------| +| `core/` 核心基础模块 | ≥ 90% | +| `apps/*/services/` 业务逻辑层 | ≥ 80% | +| `apps/*/views.py` 视图层 | ≥ 70% | +| `apps/*/tasks.py` 异步任务 | ≥ 70% | +| E2E 核心用户旅程 | 5 条必须全部通过 | + +--- + +## 2. 测试分层架构 + +Fonrey 采用三层测试体系,从底层向上覆盖: + +``` +┌─────────────────────────────────────────┐ +│ E2E 测试(用户行为模拟) │ ← Playwright +│ 覆盖:5 条核心用户旅程 │ +├─────────────────────────────────────────┤ +│ 集成测试(API / View 层) │ ← pytest-django TenantClient +│ 覆盖:所有 P0 User Story 的 HTTP 接口 │ +├─────────────────────────────────────────┤ +│ 单元测试(逻辑单元) │ ← pytest-django + factory_boy +│ 覆盖:core/、services/、tasks.py │ +└─────────────────────────────────────────┘ +``` + +**三层分工原则**: + +- **单元测试**:不启动 HTTP server,不依赖浏览器,速度最快;测试单一函数/类的逻辑正确性 +- **集成测试**:使用 Django 测试客户端,验证完整请求-响应链路(View → Service → DB);不启动真实 HTTP server +- **E2E 测试**:启动真实 Django dev server,用浏览器驱动验证真实用户操作流程;速度最慢,只覆盖核心旅程 + +--- + +## 3. 工具选型 + +### 3.1 工具清单 + +| 类型 | 工具 | 版本 | 用途 | +|------|------|------|------| +| 测试框架 | `pytest` | ≥ 8.x | 统一测试运行器 | +| Django 集成 | `pytest-django` | ≥ 4.x | Django 数据库、Client、设置管理 | +| 测试数据工厂 | `factory_boy` | ≥ 3.x | 创建测试用 Model 实例,避免手写 fixture | +| 假数据生成 | `Faker` | ≥ 25.x | 生成中文姓名、手机号、地址等假数据 | +| Mock 工具 | `pytest-mock` | ≥ 3.x | Mock 外部依赖(R2、Redis、邮件服务) | +| HTTP Mock | `responses` | ≥ 0.25.x | Mock 第三方 HTTP 请求(Cloudflare API 等) | +| E2E 测试 | `playwright` (Python) | ≥ 1.44.x | 浏览器自动化 | +| E2E 集成 | `pytest-playwright` | ≥ 0.5.x | Playwright 的 pytest 插件 | +| 覆盖率 | `pytest-cov` | ≥ 5.x | 生成代码覆盖率报告 | +| 并行加速 | `pytest-xdist` | ≥ 3.x | 多进程并行运行单元/集成测试 | + +### 3.2 安装依赖 + +所有测试依赖统一放在 `requirements/test.txt`: + +``` +pytest>=8.0 +pytest-django>=4.8 +pytest-mock>=3.12 +pytest-cov>=5.0 +pytest-xdist>=3.5 +pytest-playwright>=0.5 +factory_boy>=3.3 +Faker>=25.0 +responses>=0.25 +``` + +安装命令: + +```bash +pip install -r requirements/test.txt +playwright install chromium +``` + +--- + +## 4. 目录结构 + +``` +fonrey/ +└── tests/ + ├── conftest.py # 全局 fixtures(租户、用户、客户端) + ├── settings_test.py # 测试专用 Django settings + ├── factories/ # factory_boy 工厂 + │ ├── __init__.py + │ ├── tenant_factory.py # Tenant、域名 + │ ├── account_factory.py # Staff、Account + │ ├── org_factory.py # OrgUnit + │ ├── permission_factory.py # Role、Permission + │ ├── complex_factory.py # Complex、Building + │ ├── property_factory.py # Property、FollowUpLog + │ └── client_factory.py # Client、ClientFollowUp + ├── unit/ # 单元测试 + │ ├── test_encryption.py # PII 加密/解密 + │ ├── test_soft_delete.py # 软删除 Manager + │ ├── test_permission_service.py + │ ├── test_property_service.py + │ ├── test_client_service.py + │ └── test_celery_tasks.py # Celery 任务(同步模式) + ├── integration/ # 集成测试(按 User Story 分文件) + │ ├── account/ + │ │ └── test_us_account.py # US-ACCOUNT-001~003 + │ ├── permission/ + │ │ └── test_us_permission.py # US-PERMISSION-001~005 + │ ├── complex/ + │ │ └── test_us_complex.py + │ ├── property/ + │ │ └── test_us_property.py # US-PROPERTY-001~008 + │ ├── client/ + │ │ └── test_us_client.py # US-CLIENT-001~017 + │ ├── org/ + │ │ └── test_us_org.py + │ └── setting/ + │ └── test_us_setting.py + └── e2e/ # E2E 测试(核心用户旅程) + ├── conftest.py # E2E 专用 fixtures(live_server、page) + ├── test_journey_login.py + ├── test_journey_property.py + ├── test_journey_client.py + ├── test_journey_permission.py + └── test_journey_onboarding.py +``` + +--- + +## 5. 多租户测试约定 + +这是 Fonrey 测试中最重要的约定。`django-tenants` 的 Schema 隔离在测试中必须正确处理,否则测试结果不可信。 + +### 5.1 核心原则 + +- **所有集成测试和单元测试**(涉及数据库的)必须在租户 Schema 上下文中执行 +- 严禁在 `public` Schema 下直接操作业务数据 +- 每个测试函数执行后,数据库状态自动回滚(`pytest-django` 的 `db` fixture 保证事务隔离) +- 禁止测试之间共享可变状态(禁止 `module` 或 `session` 级别的数据库 fixtures,除非明确只读) + +### 5.2 租户 Fixture 规范 + +全局 `conftest.py` 必须提供以下标准 fixtures: + +```python +# tests/conftest.py(规范示意,非最终代码) + +@pytest.fixture(scope="session") +def tenant(): + """ + 创建一个测试租户(session 级别,全程复用同一个 Schema)。 + 使用 django_tenants.test.client.TenantClient 配套使用。 + """ + +@pytest.fixture +def tenant_client(tenant): + """ + 返回绑定到测试租户的 TenantClient 实例。 + 等价于 Django 的 Client(),但自动切换到租户 Schema。 + 所有集成测试的 HTTP 请求必须通过此 client 发出。 + """ + +@pytest.fixture +def staff_user(tenant): + """普通员工用户,已完成登录态(含 Cookie/Session)""" + +@pytest.fixture +def admin_user(tenant): + """系统管理员用户""" + +@pytest.fixture +def authenticated_client(tenant_client, staff_user): + """已登录状态的 TenantClient""" +``` + +### 5.3 禁止事项 + +- ❌ 禁止在测试中使用 Django 原生 `Client()`,必须使用 `TenantClient` +- ❌ 禁止在测试中手动 `SET search_path`,由 fixtures 统一管理 +- ❌ 禁止跨租户数据访问断言(每个测试只能操作自己的租户数据) + +--- + +## 6. 单元测试规范 + +### 6.1 适用范围 + +单元测试覆盖以下代码,**不依赖 HTTP 请求,速度要求 < 100ms/个**: + +| 目标代码 | 测试文件位置 | +|---------|------------| +| `core/encryption.py` | `tests/unit/test_encryption.py` | +| `core/models/base.py`(软删除、ActiveManager) | `tests/unit/test_soft_delete.py` | +| `apps/*/services/` 所有 service 函数 | `tests/unit/test_*_service.py` | +| `apps/*/tasks.py` Celery 任务 | `tests/unit/test_celery_tasks.py` | +| `core/cache.py` Redis key 工具函数 | `tests/unit/test_cache.py` | + +### 6.2 factory_boy 规范 + +每个 Django Model 必须有对应的 Factory,集中放在 `tests/factories/` 下: + +- Factory 类名统一为 `{ModelName}Factory` +- 使用 `faker` 生成中文假数据(姓名、手机号、地址) +- 手机号字段必须使用未加密的明文值传入 factory(factory 内部触发 Model 的加密逻辑) +- Factory 之间通过 `SubFactory` 表达依赖关系,禁止在 Factory 内部硬编码 ID + +### 6.3 Celery 任务测试规范 + +所有 Celery 任务测试必须在同步模式下运行,在 `settings_test.py` 中配置: + +```python +# tests/settings_test.py +CELERY_TASK_ALWAYS_EAGER = True # 任务同步执行 +CELERY_TASK_EAGER_PROPAGATES = True # 同步模式下抛出真实异常 +``` + +调用方式统一使用 `.apply()` 而非 `.delay()` 或 `.apply_async()`: + +```python +# 正确 +result = my_task.apply(args=[...]) + +# 禁止在测试中使用 +my_task.delay(...) +my_task.apply_async(...) +``` + +### 6.4 PII 加密测试要求 + +`test_encryption.py` 必须覆盖以下场景: + +- 加密后的密文与明文不同 +- 相同明文每次加密产生不同密文(GCM nonce 随机性) +- 解密后的明文与原始明文完全一致 +- 加密字段的 SHA-256 hash 索引值具有确定性(相同明文产生相同 hash) +- 解密错误(篡改密文)抛出可识别异常 + +--- + +## 7. 集成测试规范 + +### 7.1 适用范围 + +集成测试覆盖完整的 HTTP 请求-响应链路,每个 P0 User Story 至少对应一个集成测试文件。 + +### 7.2 HTMX 请求约定 + +Fonrey 的 View 层分为两种响应模式,测试必须对应覆盖: + +| 请求类型 | Header | 预期响应 | +|---------|--------|---------| +| 普通页面请求 | 无 | 完整 HTML(含 ``, ``, ``) | +| HTMX 局部请求 | `HTTP_HX_REQUEST: true` | 局部 HTML 片段(不含完整页面结构) | + +HTMX 请求在 `TenantClient` 中发送方式: + +```python +# HTMX 局部请求(规范示意) +response = authenticated_client.get( + '/properties/', + HTTP_HX_REQUEST='true' +) + +# 验证返回局部 HTML(不含完整页面标签) +assert ' `text` > `placeholder` > `data-testid` > CSS 选择器(可维护性从高到低) +- 断言使用 `expect()` 而非原生 `assert`,获得更清晰的错误输出 + +### 8.3 HTMX 页面的 E2E 注意事项 + +HTMX 局部更新后,DOM 发生变化但页面 URL 可能不变。等待策略: + +```python +# 触发 HTMX 请求后,等待网络空闲(HTMX 请求完成) +page.click('button:has-text("筛选")') +page.wait_for_load_state('networkidle') +# 再断言 DOM 内容 +expect(page.locator('.property-list')).to_contain_text('...') +``` + +### 8.4 E2E 测试数据管理 + +- E2E 测试使用独立的测试租户(在 `tests/e2e/conftest.py` 中创建) +- 每次 E2E 测试套件运行前,重置测试租户数据至初始种子状态 +- 禁止 E2E 测试依赖其他 E2E 测试的产出数据(每条旅程测试自行准备数据) + +--- + +## 9. 测试配置文件 + +### 9.1 pytest.ini + +```ini +[pytest] +DJANGO_SETTINGS_MODULE = tests.settings_test +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + --tb=short + --strict-markers + -q +markers = + unit: 单元测试(不访问数据库) + integration: 集成测试(访问数据库,使用 TenantClient) + e2e: E2E 测试(启动真实服务,需要浏览器) + slow: 耗时超过 5 秒的测试 +``` + +### 9.2 tests/settings_test.py 关键配置 + +```python +# 继承主 settings,覆盖以下配置 + +DATABASES = { + # 使用独立的测试数据库(CI 中由环境变量注入) +} + +# Celery 同步模式 +CELERY_TASK_ALWAYS_EAGER = True +CELERY_TASK_EAGER_PROPAGATES = True + +# 使用内存缓存(避免依赖真实 Redis) +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + } +} + +# 邮件使用内存后端 +EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + +# 文件存储使用本地临时目录(非 R2) +DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +MEDIA_ROOT = '/tmp/fonrey_test_media/' + +# 关闭密码哈希加速(测试中不需要高强度 hash) +PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] + +# 禁用 DEBUG(贴近生产环境) +DEBUG = False +``` + +--- + +## 10. CI 自动化运行 + +### 10.1 GitHub Actions 配置 + +每日凌晨 2 点自动运行全量测试套件,并在每次 push 到 `main` / `develop` 分支时触发: + +```yaml +# .github/workflows/daily-test.yml +name: Daily Test Suite + +on: + schedule: + - cron: '0 18 * * *' # UTC 18:00 = 北京时间次日 02:00 + push: + branches: [main, develop] + +jobs: + unit-and-integration: + name: Unit & Integration Tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: fonrey_test + POSTGRES_USER: fonrey + POSTGRES_PASSWORD: test_password + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + - run: pip install -r requirements/test.txt + - name: Run unit tests + run: pytest tests/unit -m unit --cov=core --cov=apps -q + - name: Run integration tests + run: pytest tests/integration -m integration -q + - name: Upload coverage report + uses: codecov/codecov-action@v4 + + e2e: + name: E2E Tests (Core Journeys) + runs-on: ubuntu-latest + needs: unit-and-integration + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + - run: pip install -r requirements/test.txt + - run: playwright install chromium --with-deps + - name: Run E2E tests + run: pytest tests/e2e -m e2e -q + - name: Upload E2E screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-screenshots + path: tests/e2e/screenshots/ +``` + +### 10.2 本地每日运行 + +开发机本地运行全量测试: + +```bash +# scripts/daily_test.sh + +#!/bin/bash +set -e + +echo "==============================" +echo " Fonrey Daily Test Runner" +echo "==============================" + +echo "" +echo "[1/3] 单元测试..." +pytest tests/unit -m unit -q --tb=short + +echo "" +echo "[2/3] 集成测试..." +pytest tests/integration -m integration -q --tb=short + +echo "" +echo "[3/3] E2E 核心旅程测试..." +pytest tests/e2e -m e2e -q --tb=short + +echo "" +echo "[覆盖率报告]" +pytest tests/unit tests/integration \ + --cov=core --cov=apps \ + --cov-report=term-missing \ + --cov-fail-under=70 \ + -q --tb=no + +echo "" +echo "==============================" +echo " All tests passed." +echo "==============================" +``` + +--- + +## 11. AI 辅助编码时的测试要求 + +在 Fonrey vibe coding 流程中,每次 AI 完成一个 User Story 的功能代码后,**必须同步产出对应测试**,不允许欠测试债。 + +### 11.1 每个 User Story 的测试产出清单 + +AI 完成功能代码后,须产出以下内容(缺一不可): + +- [ ] 相关 Model 的 `factory_boy` Factory(若尚未存在) +- [ ] Service 层的单元测试(正常路径 + 至少 2 个边界/异常场景) +- [ ] View 层的集成测试(覆盖 PRD 验收标准中所有 AC 条目) +- [ ] 权限场景覆盖(有权限 / 无权限 / 未登录,见 §7.3) +- [ ] HTMX 局部请求与完整页面请求分别测试(见 §7.2) + +### 11.2 触发 AI 生成测试的标准 Prompt 模板 + +``` +基于刚才实现的 [US-XXX-NNN],请为其生成完整测试: + +1. factory_boy 工厂(如尚未存在) + - 文件位置:tests/factories/{app}_factory.py + - 使用 Faker 生成中文假数据 + +2. Service 层单元测试 + - 文件位置:tests/unit/test_{app}_service.py + - 覆盖正常路径 + 至少 2 个边界/异常场景 + - 使用 pytest-mock mock 外部依赖 + +3. View 层集成测试 + - 文件位置:tests/integration/{app}/test_us_{module}.py + - 覆盖 PRD 中该 US 的所有 AC 条目 + - 使用 TenantClient 发送请求 + - HTMX 请求和普通请求分别覆盖 + - 权限场景:有权限 / 无权限(403) / 未登录(302) + +所有测试须遵循 TECH_STACK/测试规范.md 中的约定。 +租户 fixtures 从 tests/conftest.py 导入,不要重复定义。 +``` + +### 11.3 测试失败时的修复流程 + +CI 测试失败后,AI 修复流程: + +1. 读取失败的测试输出(`--tb=short` 格式) +2. 定位失败原因(逻辑错误 / 数据错误 / 环境依赖问题) +3. **优先修复功能代码**(测试是需求的正式表达,不轻易修改测试) +4. 仅当测试本身有误(如 AC 理解错误)时才修改测试,并注明修改原因 +5. 修复后本地重跑对应测试套件确认通过,再提交 + +--- + +## 12. 禁止项(Do NOT) + +- ❌ 禁止使用 Django 原生 `Client()`,必须使用 `TenantClient` +- ❌ 禁止使用固定等待 `time.sleep()` 或 `page.wait_for_timeout()` +- ❌ 禁止测试直接调用真实外部服务(R2、邮件、第三方 API) +- ❌ 禁止测试之间共享可变数据(避免测试顺序依赖) +- ❌ 禁止在测试中硬编码 Tenant ID、UUID、时间戳 +- ❌ 禁止 E2E 测试依赖其他 E2E 测试产出的数据 +- ❌ 禁止跳过权限验证场景(无权限 / 未登录场景必须覆盖) +- ❌ 禁止在功能代码未完成时先写空测试(`pass` 占位)后忘记补全 + +--- + +## 13. 文档索引 + +| 文档 | 说明 | +|------|------| +| [`TECH_STACK.md`](./TECH_STACK.md) | 技术栈总纲 | +| [`登录管理技术方案.md`](./登录管理技术方案.md) | 登录模块技术细节 | +| [`权限管理系统技术方案.md`](./权限管理系统技术方案.md) | 权限模块技术细节 | +| [`../PRD/TASK.md`](../PRD/TASK.md) | P0 User Story 清单(测试覆盖基准) | +| [`../DATA_MODEL/DATA_MODEL.md`](../DATA_MODEL/DATA_MODEL.md) | 数据模型总览(factory 设计参考) | diff --git a/Project/fonrey/TECH_STACK/登录管理技术方案.md b/Project/fonrey/TECH_STACK/登录管理技术方案.md index b711efc9..f004acde 100644 --- a/Project/fonrey/TECH_STACK/登录管理技术方案.md +++ b/Project/fonrey/TECH_STACK/登录管理技术方案.md @@ -1,711 +1,711 @@ -> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked. -# Fonrey 登录管理系统技术方案 - -**版本**: 2.0 | **项目**: Fonrey 房产经纪管理系统 | **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL + Redis + Celery -**关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v1.3) -**关联数据模型**: `Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` -**最后更新**: 2026-04-25(v2.0 补充服务层设计、HTMX 交互模式、Celery 任务、错误处理规范) - -> **For AI assistants**: Read this entire file before writing any code. -> All decisions here are final. Do not suggest alternatives unless asked. - ---- - -## 一、模块定位与架构边界 - -登录管理模块(`accounts` App)负责多租户环境下的身份识别、认证、账号安全及凭据找回。 - -### 架构层级边界 - -| 层级 | 位置 | 说明 | -|------|------|------| -| Tenant ID 验证 | `shared_apps`(Public Schema) | 属于平台基础服务,在 `public` schema 下运行,无需租户切换 | -| 账号认证、找回密码等 | 租户 Schema(Tenant Schema) | 通过请求域名 `{tenant_slug}.fonrey.com` 自动切换,`django-tenants` 中间件处理 | -| Electron 客户端 | 前端 | 负责 Tenant ID 本地缓存、Session 管理、页面加载 | - -### 模块依赖关系 - -``` -accounts - ├── 依赖 → org (Staff 实名绑定,单向依赖) - ├── 依赖 → core.encryption (手机号加密) - ├── 依赖 → core.cache (Redis 工具封装) - ├── 依赖 → shared.tenants (Tenant ID 验证,Public Schema) - └── 被依赖 ← org (离职联动,通过 Service 层调用) -``` - ---- - -## 二、依赖与技术选型 - -| 依赖项 | 版本/方案 | 用途 | 说明 | -|--------|-----------|------|------| -| `django.contrib.auth` | Django 内置 | 用户认证基础框架 | 扩展 `AbstractBaseUser`,**不直接使用** `User` 模型;username 唯一性约束在租户 Schema 维度生效 | -| `django-tenants` | 已有 | 多租户隔离 | `UserAccount` 在租户 Schema;Tenant 验证接口在 `shared_apps` | -| `PostgreSQL` | 已有 | 数据持久化 | Schema 级别隔离租户数据 | -| `Redis` | 必须 | 多用途缓存 | 滑块验证 Token(TTL 3min)、登录失败计数(TTL 30min)、密码重置频率限制 | -| `Celery` | 必须 | 异步任务队列 | 邮件发送异步处理,防止登录/找回接口超时(邮件发送可能耗时 > 500ms) | -| `Pillow` | 必须(若自研验证码) | 图片处理 | 生成拼图背景图(抠出缺口)+ 拼图碎片,输出 Base64 | -| `django-ratelimit` 或自定义中间件 | 必须 | 接口限流 | Tenant 验证、登录、找回密码接口均需限流 | -| `electron-store` 或 AES 加密文件 | Electron 侧 | 本地持久化 | 加密存储 Tenant ID,不存明文;路径为 `app.getPath('userData')` | -| `secrets` (Python 标准库) | Python 内置 | Token 生成 | 使用 `secrets.token_urlsafe(64)` 生成密码重置 Token(86 字符) | - -### 滑块验证码方案选型(待确认,见开放问题) - -| 方案 | 优点 | 缺点 | -|------|------|------| -| 自研(Pillow + 前端拖拽组件) | 完全可控,无外部依赖,数据合规性好 | 需维护图库,需自己实现轨迹检测算法 | -| 第三方服务(极验 GeeTest / 网易易盾) | 开箱即用,安全性更高 | 引入外部依赖,有数据合规风险,需评估 | - -**当前方案**:暂按自研设计,后端负责人需在开发启动前确认最终选型。 - ---- - -## 三、目录结构 - -``` -fonrey/apps/ -└── accounts/ # 账号认证管理(租户级 App) - ├── models.py # UserAccount, LoginAttempt, PasswordResetToken, PasswordHistory - ├── views/ - │ ├── auth.py # 登录/登出视图(HTMX 响应) - │ ├── captcha.py # 滑块验证码视图 - │ └── recovery.py # 找回用户名/密码视图 - ├── urls.py - ├── serializers.py # API 序列化(JSON 接口,供 Electron 前端使用) - ├── forms.py # 登录表单、找回密码表单 - ├── templates/ - │ └── accounts/ - │ ├── login.html # 登录页(含滑块验证码区域) - │ ├── tenant_verify.html # Tenant 识别页(首次启动) - │ ├── change_password.html # 强制修改初始密码页 - │ ├── recover_username.html # 找回用户名页 - │ ├── recover_password.html # 找回密码(步骤 1:身份验证) - │ └── reset_password.html # 重置密码(步骤 2:设置新密码) - └── services/ - ├── auth.py # 认证逻辑:滑块验证、账号锁定、登录流程 - ├── captcha.py # 验证码生成与校验(Pillow 或第三方) - ├── recovery.py # 找回用户名/密码逻辑(含 Celery 任务触发) - ├── password.py # 密码复杂度校验、历史密码比对 - └── tenant.py # Tenant 验证逻辑(属于 shared_apps,Public Schema) - -fonrey/shared/ # Public Schema App(django-tenants shared_apps) -└── tenants/ - ├── models.py # TenantModel, Domain - └── views.py # tenant/verify/ 接口(在公共 Schema 下) -``` - ---- - -## 四、数据模型 - -> **数据模型完整定义已迁移至** `Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`,本节仅保留技术实现视角的关键说明。 - -### 4.1 表归属汇总 - -| 表名 | Schema | 说明 | -|------|--------|------| -| `user_accounts` | 租户 Schema | 账号主表,`username` 唯一性在 Schema 维度生效 | -| `login_attempts` | 租户 Schema | 登录审计,保留 ≥ 90 天 | -| `password_reset_tokens` | 租户 Schema | 一次性重置令牌,30 分钟过期 | -| `password_histories` | 租户 Schema | 最近 3 次密码哈希,防重用 | - -### 4.2 关键约束汇总 - -- `username` 唯一性约束仅在当前租户 Schema 内生效(`django-tenants` 隔离机制),**不同租户可以有相同 username** -- 密码存储使用 Django 默认 `PBKDF2+SHA256`(`make_password`),**后端不得明文存储或传输** -- `phone_enc` 字段使用 `core.encryption` AES-256-GCM 加密存储;`phone_hash` SHA-256 哈希用于唯一性校验 -- `locked_until` 字段持久化锁定到期时间,防止 Redis 故障导致锁定状态丢失 - ---- - -## 五、服务层设计(Service Layer) - -### 5.1 `services/auth.py` — 核心认证服务 - -```python -# apps/accounts/services/auth.py - -class AuthService: - - LOGIN_FAIL_LIMIT = 5 # 连续失败次数触发锁定 - LOCK_DURATION_MINUTES = 30 # 锁定时长(分钟) - - @classmethod - def authenticate(cls, username: str, password: str, captcha_pass_token: str, - tenant_id: str, ip_address: str, user_agent: str) -> dict: - """ - 完整登录流程: - 1. 校验 captcha_pass_token(一次性凭证,Redis 查询后立即删除) - 2. 查询账号(不存在则记录审计日志,返回通用错误) - 3. 检查账号状态(locked / disabled) - 4. 校验密码 - 5. 登录成功后:更新 last_login,清零失败计数,返回账号信息 - 6. 失败时:递增失败计数,超限触发锁定 - - Returns: - {'success': True, 'user': UserAccount, 'is_initial_password': bool} - {'success': False, 'error_code': str, 'error_message': str} - """ - ... - - @classmethod - def _check_lock_status(cls, user: 'UserAccount') -> bool: - """检查账号锁定状态,自动解锁已到期的锁定""" - ... - - @classmethod - def _increment_fail_count(cls, tenant_id: str, username: str) -> int: - """递增失败计数,返回当前计数;超限时触发账号锁定""" - ... - - @classmethod - def _trigger_lock(cls, user: 'UserAccount') -> None: - """触发账号锁定:status=locked, locked_until=now+30min""" - ... - - @classmethod - def unlock_account(cls, user: 'UserAccount') -> None: - """管理员手动解锁账号""" - ... -``` - -### 5.2 `services/captcha.py` — 验证码服务 - -```python -# apps/accounts/services/captcha.py - -class CaptchaService: - - CAPTCHA_TTL_SECONDS = 180 # 验证会话有效期(3分钟) - PASS_TOKEN_TTL_SECONDS = 180 # 通过凭证有效期(3分钟) - - @classmethod - def generate(cls) -> dict: - """ - 生成滑块拼图验证码。 - Returns: - { - 'session_token': str, # Redis Key uuid,供前端提交时携带 - 'background_b64': str, # 背景图(含缺口)Base64 - 'puzzle_b64': str, # 拼图碎片 Base64 - 'gap_y': int, # 缺口 Y 坐标(前端定位碎片初始位置) - } - 注意:缺口 X 坐标(gap_x)不返回给前端,服务端保存在 Redis。 - """ - ... - - @classmethod - def verify(cls, session_token: str, slide_x: int, trajectory: list) -> dict: - """ - 校验滑动结果。 - Args: - session_token: generate() 返回的会话标识 - slide_x: 用户最终滑动距离(px) - trajectory: 滑动轨迹,格式 [{'x': int, 'y': int, 't': int}, ...] - Returns: - {'pass': True, 'pass_token': str} # 通过,pass_token 用于登录接口 - {'pass': False, 'message': str} # 失败,前端自动刷新拼图 - 校验规则: - 1. 位置偏差:abs(slide_x - gap_x) <= 5px - 2. 轨迹特征:存在加速→减速曲线,拒绝匀速/程序化轨迹 - """ - ... -``` - -### 5.3 `services/recovery.py` — 找回账号服务 - -```python -# apps/accounts/services/recovery.py - -class RecoveryService: - - RESET_LINK_EXPIRE_MINUTES = 30 - MAX_EMAILS_PER_HOUR = 3 - - @classmethod - def request_username_recovery(cls, email: str) -> None: - """ - 发起找回用户名。 - - 无论邮箱是否存在,统一返回「如该邮箱已绑定账号,您将收到邮件」 - - 邮箱存在时:触发 Celery 任务异步发送邮件 - - 限频:同一邮箱 1 小时内最多 3 次(Redis 计数) - """ - ... - - @classmethod - def request_password_reset(cls, username: str, email: str) -> None: - """ - 发起找回密码(步骤 1)。 - - 无论匹配结果,统一返回「如信息匹配,重置链接将发送至邮箱」(防枚举) - - 匹配成功时:生成 PasswordResetToken,触发 Celery 异步发送邮件 - - 限频:同一账号 1 小时内最多 3 次(Redis 计数) - """ - ... - - @classmethod - def reset_password(cls, token_str: str, new_password: str) -> dict: - """ - 重置密码(步骤 2)。 - Returns: - {'success': True} - {'success': False, 'error_code': 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'PASSWORD_REUSED'} - 操作顺序: - 1. 查询并校验 token(is_used=False, expires_at > now) - 2. 校验密码复杂度 - 3. 校验历史密码(最近 3 次) - 4. 更新密码哈希,is_initial_password=False - 5. 标记 token is_used=True - 6. 清除该账号所有有效 Session(强制重新登录) - 7. 写入 PasswordHistory - """ - ... -``` - -### 5.4 `services/password.py` — 密码规则服务 - -```python -# apps/accounts/services/password.py - -class PasswordService: - - MIN_LENGTH = 8 - MAX_LENGTH = 32 - HISTORY_COUNT = 3 # 保留最近 N 条历史密码 - - @classmethod - def validate_complexity(cls, password: str) -> list[str]: - """ - 校验密码复杂度。 - Returns: 错误列表(空列表表示通过) - 规则: - - 长度 8~32 位 - - 必须包含字母(区分大小写) - - 必须包含数字 - """ - ... - - @classmethod - def check_history(cls, user: 'UserAccount', new_password: str) -> bool: - """ - 检查新密码是否与最近 3 次历史密码重复。 - Returns: True(允许使用)/ False(与历史重复) - """ - ... - - @classmethod - def save_history(cls, user: 'UserAccount', password_hash: str) -> None: - """ - 保存新密码哈希至历史记录,超出 HISTORY_COUNT 时删除最旧记录。 - """ - ... -``` - ---- - -## 六、Celery 异步任务 - -```python -# apps/accounts/tasks.py - -from celery import shared_task - -@shared_task( - name='accounts.send_username_recovery_email', - max_retries=3, - default_retry_delay=60, # 失败后 60 秒重试 -) -def send_username_recovery_email(email: str, username: str, company_name: str) -> None: - """ - 发送找回用户名邮件。 - 失败时自动重试最多 3 次;3 次均失败则写入告警日志(Sentry)。 - 邮件内容:用户名 + 发送时间 + 联系管理员说明。 - """ - ... - - -@shared_task( - name='accounts.send_password_reset_email', - max_retries=3, - default_retry_delay=60, -) -def send_password_reset_email(email: str, reset_link: str, company_name: str, - expires_at: str) -> None: - """ - 发送密码重置链接邮件。 - 失败时自动重试最多 3 次;3 次均失败则写入告警日志(Sentry)。 - 邮件内容:重置链接(30分钟有效)+ 安全说明。 - """ - ... -``` - -> **重试策略**:邮件发送失败时不向前端返回错误(用户已看到「邮件已发送」提示),在后台静默重试;3 次重试均失败后通过 Sentry 上报告警,管理员可在后台查看 Token 手动告知用户。 - ---- - -## 七、接口清单 - -| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 限流规则 | 响应格式 | 说明 | -|------|------|------------|------------|---------|---------|------| -| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | 每 IP 每分钟 ≤ 10 次 | JSON | Tenant ID 验证 | -| `/api/auth/captcha/` | GET | Tenant | 否 | — | JSON | 获取滑块拼图验证码 | -| `/api/auth/captcha/verify/` | POST | Tenant | 否 | — | JSON | 提交滑动轨迹,返回一次性通过凭证 | -| `/api/auth/login/` | POST | Tenant | 否 | 每 IP 每分钟 ≤ 20 次 | JSON | 账号密码登录 | -| `/api/auth/logout/` | POST | Tenant | 是 | — | JSON | 登出,使服务端 Session 失效 | -| `/api/auth/password/change/` | POST | Tenant | 是 | — | JSON / HTMX | 强制修改初始密码(登录后跳转) | -| `/api/auth/recover/username/` | POST | Tenant | 否 | 每邮箱每小时 ≤ 3 次 | JSON / HTMX | 发起找回用户名(发送邮件) | -| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 每账号每小时 ≤ 3 次 | JSON / HTMX | 发起找回密码(发送重置链接邮件) | -| `/api/auth/recover/password/reset/` | POST | Tenant | 否(Token 鉴权) | — | JSON / HTMX | 提交新密码,使用 PasswordResetToken 校验 | -| `/api/auth/login/phone/` | POST | Tenant | 否 | — | JSON | **预留**,v2 实现,手机验证码登录 | -| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | — | JSON | **预留**,v2 实现,获取微信二维码 | -| `/api/auth/wechat/callback/` | POST | Tenant | 否 | — | JSON | **预留**,v2 实现,微信扫码回调 | - -### 7.1 Tenant 验证接口规范 - -``` -POST /api/auth/tenant/verify/ - -Request Body: -{ - "tenant_id": "202500010001" // 固定 12 位纯数字 -} - -Response 200 (验证通过): -{ - "valid": true, - "tenant_name": "XX房产经纪有限公司", - "tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png", - "login_url": "https://xxx.fonrey.com/auth/login/" -} - -Response 200 (验证失败): -{ - "valid": false, - "error_code": "TENANT_NOT_FOUND", - "message": "识别码无效" -} -``` - -> 失败响应统一返回 HTTP 200,不区分「未找到」与「已禁用」,防止枚举攻击。 - -### 7.2 登录接口规范 - -``` -POST /api/auth/login/ - -Request Body: -{ - "username": "string", - "password": "string", - "captcha_pass_token": "string" // 滑块验证通过后的一次性凭证(UUID) -} - -Response 200 (登录成功): -{ - "success": true, - "token": "...", - "user": { - "id": 1, - "username": "...", - "display_name": "...", - "is_initial_password": false - } -} - -Response 200 (登录失败): -{ - "success": false, - "error_code": "WRONG_CREDENTIALS" | "ACCOUNT_LOCKED" | "ACCOUNT_DISABLED" | "CAPTCHA_INVALID", - "message": "...", - "lock_remaining_seconds": 1800 // 仅 ACCOUNT_LOCKED 时返回 -} -``` - -> **注意**:`WRONG_CREDENTIALS` 不区分「用户名错误」与「密码错误」,防止枚举攻击。 - -### 7.3 验证码接口规范 - -``` -GET /api/auth/captcha/ - -Response 200: -{ - "session_token": "uuid-string", // 提交验证时携带 - "background_b64": "data:image/png;base64,...", // 带缺口的背景图 - "puzzle_b64": "data:image/png;base64,...", // 拼图碎片 - "gap_y": 120, // 缺口 Y 坐标(用于定位碎片初始位置) - "width": 320, // 背景图宽度(px) - "height": 160 // 背景图高度(px) -} - -POST /api/auth/captcha/verify/ - -Request Body: -{ - "session_token": "uuid-string", - "slide_x": 185, // 最终滑动距离(px) - "trajectory": [ - {"x": 0, "y": 0, "t": 0}, - {"x": 20, "y": 1, "t": 80}, - {"x": 185, "y": 2, "t": 1200} - ] -} - -Response 200 (验证通过): -{ - "pass": true, - "pass_token": "uuid-string" // 一次性凭证,TTL 3分钟,登录时携带 -} - -Response 200 (验证失败): -{ - "pass": false, - "message": "验证失败,请重新拖动" -} -``` - ---- - -## 八、前端交互模式(HTMX + Alpine.js) - -### 8.1 页面结构说明 - -登录相关页面均为**全页面渲染**(Server-Side Rendered),Electron 客户端通过 `BrowserWindow.loadURL()` 加载完整 HTML。登录流程中的局部交互(如验证码刷新、错误提示)通过 HTMX 局部刷新实现。 - -### 8.2 登录页核心交互 - -```html - - - -
- -
- 验证图片 -
-
-
- - -
-
- 拖动完成拼图 - 验证通过 ✓ -
-
- - - -
- - -
- - - - -
-
-
登录中...
-``` - -> **Alpine.js 职责**:管理验证码状态(加载中/通过/失败)、滑动轨迹记录、`pass_token` 绑定到表单隐藏字段。 -> **HTMX 职责**:表单提交、错误反馈局部渲染(`hx-target="#login-feedback"`)。 - -### 8.3 HTMX 响应片段规范 - -登录接口在 HTMX 请求时(`HX-Request: true` Header)返回 HTML 片段而非 JSON,供 `hx-target` 局部替换: - -**登录成功**(服务端返回 302 重定向,HTMX 通过 `HX-Redirect` Header 处理): -```python -# views/auth.py -if request.headers.get('HX-Request'): - response = HttpResponse() - response['HX-Redirect'] = '/dashboard/' # 跳转首页 - return response -``` - -**登录失败**(返回错误提示 HTML 片段): -```html - -
- 用户名或密码错误,请重新输入 -
-``` - -**初始密码状态**(登录成功但需修改密码): -```python -if request.headers.get('HX-Request'): - response = HttpResponse() - response['HX-Redirect'] = '/auth/password/change/' - return response -``` - ---- - -## 九、Redis Key 规范 - -| 用途 | Key 格式 | 类型 | TTL | 说明 | -|------|----------|------|-----|------| -| 滑块验证会话(含缺口位置) | `captcha_session:{uuid}` | HASH | 3 分钟 | 存储 `gap_x`, `session_token`;验证后立即删除 | -| 滑块验证通过凭证 | `captcha_pass:{uuid}` | STRING | 3 分钟 | 登录接口验证后立即删除(单次有效) | -| 登录失败计数 | `login_fail:{tenant_id}:{username}` | STRING | 30 分钟 | 计数 ≥ 5 时触发锁定;TTL 30 分钟自动清零 | -| 找回邮件发送频率 | `recover_email:{email}` | STRING | 1 小时 | 记录已发送次数,上限 3 次/小时 | -| 密码重置 Token 生成频率 | `recover_reset:{user_id}` | STRING | 1 小时 | 同一账号生成次数,上限 3 次/小时 | -| Tenant ID 限流 | `tenant_verify_ip:{ip}` | STRING | 1 分钟 | 计数 ≥ 10 时拒绝请求 | - -> **故障恢复**:Redis 重启后,登录失败计数归零(用户可正常登录);账号锁定状态由 `user_accounts.locked_until` 持久化保证,不依赖 Redis。 - ---- - -## 十、安全机制设计 - -### 10.1 滑块拼图验证码 - -- **图片生成**:`Pillow` 从预置图库随机抽取背景图,服务端随机生成缺口位置,抠出缺口并生成拼图碎片,两者分别以 Base64 返回前端 -- **缺口位置保护**:`gap_x`(水平位置)仅存于服务端 Redis,**不返回给前端**;前端通过 `slide_x` 提交,服务端对比 `gap_x` 校验 -- **轨迹校验**(双重判断): - - **位置偏差**:`abs(slide_x - gap_x) ≤ 5px` - - **轨迹特征**:速度变化曲线存在加速→减速(人类滑动特征),拒绝匀速/程序化轨迹 -- **独立计数**:验证码失败**不计入**账号密码错误次数,两者独立计数 -- **单次有效**:`captcha_pass_token` TTL 3 分钟,登录接口校验后立即删除 - -### 10.2 账号锁定机制 - -``` -同一账号连续密码错误 ≥ 5 次: - 1. Redis `login_fail:{tenant_id}:{username}` 计数达到阈值 - 2. 更新 user_accounts.status = 'locked' - 3. 设置 user_accounts.locked_until = now() + 30min - 4. 锁定状态下,登录接口直接返回 ACCOUNT_LOCKED,不再校验密码 - -解锁条件(任一满足): - A. locked_until 到期:应用层在下次登录时检测,自动恢复 status=active - B. Tenant Admin 手动解锁:调用 AuthService.unlock_account() -``` - -### 10.3 密码安全 - -| 规则 | 说明 | -|------|------| -| 存储哈希 | Django `PBKDF2+SHA256`(`make_password`) | -| 传输安全 | 强制 HTTPS,前端**不加密**密码(HTTPS 层保证) | -| 复杂度 | 长度 8~32 位,必须包含字母(区分大小写)+ 数字;建议特殊符号(非强制) | -| 历史密码 | 不得与最近 3 次历史密码哈希相同(含系统固定初始密码) | -| Session 有效期 | 默认 8 小时;可由 Tenant Admin 在「系统设置」中调整 | - -### 10.4 防枚举攻击设计 - -| 场景 | 防御措施 | -|------|---------| -| 登录失败 | 不区分「用户名错误」与「密码错误」,统一返回 `WRONG_CREDENTIALS` | -| 找回用户名/密码 | 无论邮箱/用户名是否存在,统一返回相同响应文案 | -| Tenant ID 验证 | 不区分「租户不存在」与「租户已禁用」;IP 限流每分钟 ≤ 10 次 | -| 密码重置 Token | Token 使用 `secrets.token_urlsafe(64)` 生成(86 字符),不可预测 | - -### 10.5 密码重置流程安全要点 - -- Token 由 `secrets.token_urlsafe(64)` 生成,86 字符,全局唯一 -- 单次有效:使用后立即标记 `is_used=True`(先标记再执行,防止并发重放) -- 有效期 30 分钟(`expires_at = created_at + timedelta(minutes=30)`) -- 重置成功后:清除该账号所有有效 Session(强制重新登录) -- 重置成功后:`is_initial_password = False`,写入 `PasswordHistory` - ---- - -## 十一、Electron 客户端约定 - -| 约定项 | 规格 | -|--------|------| -| Tenant ID 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密文件,**不存明文** | -| Session Token 存储 | 内存(`global` 变量)+ Chromium `session` Cookie,**不写入磁盘明文文件** | -| 登录页加载方式 | 主进程根据 Tenant ID 构建 `https://{tenant_slug}.fonrey.com/auth/login/`,通过 `BrowserWindow.loadURL()` 加载 | -| 多标签页 | 同一 `BrowserWindow` 内所有页面共享同一 Session Cookie | -| 客户端登出 | 调用 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie | -| 窗口关闭 | Session 保留(不自动登出),下次打开若 Session 未过期则直接进入系统 | -| 强制更新 | 客户端版本低于服务端 `min_required_version` 时,阻断登录流程,展示更新提示(详见发布管理模块 PRD) | -| Tenant ID 缓存校验 | 非首次启动时,客户端向服务端发起缓存 Tenant ID 有效性校验(`POST /api/auth/tenant/verify/`);无效则清除缓存,重新显示 Tenant 识别界面 | - ---- - -## 十二、多租户隔离要点 - -- `UserAccount`、`LoginAttempt`、`PasswordResetToken`、`PasswordHistory` 均位于**租户 Schema 内**,数据完全隔离 -- `username` 唯一性约束在 Schema 维度生效,不同租户可以存在相同 username -- Tenant 验证接口(`/api/auth/tenant/verify/`)位于 **Public Schema**(`shared_apps`),查询 `TenantModel` -- 登录等接口通过请求域名(`{tenant_slug}.fonrey.com`)自动切换 Schema,由 `django-tenants` 中间件处理,**无需手动切换** -- 所有接口禁止跨租户数据访问,ORM 查询范围自动限制在当前 Schema - ---- - -## 十三、错误处理规范 - -### 13.1 标准错误码 - -| Error Code | HTTP Status | 含义 | 前端显示文案 | -|------------|-------------|------|-------------| -| `WRONG_CREDENTIALS` | 200 | 用户名或密码错误 | 「用户名或密码错误,请重新输入」 | -| `ACCOUNT_LOCKED` | 200 | 账号已锁定 | 「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」 | -| `ACCOUNT_DISABLED` | 200 | 账号已停用 | 「账号已停用,请联系您的管理员」 | -| `CAPTCHA_INVALID` | 200 | 验证码凭证无效/已过期 | 「验证码已失效,请重新验证」 | -| `CAPTCHA_FAIL` | 200 | 滑块位置/轨迹校验失败 | 「验证失败,请重新拖动」 | -| `TENANT_NOT_FOUND` | 200 | Tenant ID 无效 | 「识别码无效,请联系您的系统管理员获取正确的识别码」 | -| `TOKEN_INVALID` | 200 | 重置 Token 无效或已使用 | 「链接已过期或已使用,请重新申请」 | -| `TOKEN_EXPIRED` | 200 | 重置 Token 已过期 | 「链接已过期,请重新申请」 | -| `PASSWORD_TOO_WEAK` | 200 | 密码不符合复杂度 | 逐条显示不满足的规则 | -| `PASSWORD_REUSED` | 200 | 密码与历史密码相同 | 「新密码不能与最近 3 次历史密码相同」 | - -> **设计原则**:所有登录相关接口统一返回 HTTP 200,通过 `error_code` 字段区分业务错误,避免 HTTP 状态码暴露系统行为(防止通过 4xx/5xx 枚举账号状态)。 - -### 13.2 异常监控 - -- 所有未预期异常(5xx)通过 Sentry 上报,含 `tenant_id`、`username`(脱敏)、堆栈信息 -- 邮件发送 Celery 任务 3 次重试失败后,上报 Sentry 告警并记录 `task_id`,管理员可在系统后台查询 - ---- - -## 十四、已知风险与缓解措施 - -| 风险 | 可能性 | 影响 | 缓解措施 | -|------|--------|------|---------| -| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征,拒绝匀速/程序化轨迹;后续可引入设备指纹 | -| Tenant ID 枚举攻击 | 低 | 中 | 限流(每 IP 每分钟 ≤ 10 次);响应不区分「未找到」与「已禁用」 | -| 密码重置 Token 泄露 | 低 | 高 | 单次有效 + 30 分钟过期 + HTTPS 传输 | -| 邮件发送失败 | 中 | 中 | 异步任务自动重试 3 次;失败写入 Sentry 告警;管理员可通过后台查看 Token 手动告知用户 | -| 多端并发登录 | 高(正常场景) | 低 | 本期允许;v2 可在 Token 引入版本号实现踢出策略 | -| Redis 故障导致锁定状态丢失 | 低 | 中 | `locked_until` 字段持久化至 PostgreSQL,Redis 故障不影响锁定判断 | - ---- - -## 十五、开放问题(开发启动前必须确认) - -| # | 问题 | 负责人 | 截止 | -|---|------|--------|------| -| 1 | 邮件服务商选型:SendGrid / 阿里云邮件推送 / SMTP 自建? | 后端负责人 + 运维 | 开发启动前 | -| 2 | 滑块验证码方案:自研(Pillow)还是第三方(极验 / 网易易盾)? | 后端负责人 + 安全 | 开发启动前 | -| 3 | Session 有效期默认值 8 小时,是否允许 Tenant Admin 自行配置? | 产品经理 | 开发启动前 | -| 4 | 账号锁定后是否自动发邮件通知用户和/或管理员? | 产品经理 | 开发启动前 | -| 5 | 历史密码校验范围:最近 3 次是否足够?是否增加「不得与用户名相同」规则? | 产品经理 | 开发启动前 | - ---- - -## 十六、明确禁止 - -- ❌ 不得使用 Django 原生 `User` 模型,必须扩展 `AbstractBaseUser` -- ❌ 不得在全局 Schema 创建 `UserAccount` 表(必须在租户 Schema 内) -- ❌ 不得明文存储或传输密码 -- ❌ 不得在 `LoginAttempt` 记录中存储密码明文(含错误密码) -- ❌ 不得在前端做密码哈希(HTTPS 层保证传输安全) -- ❌ 不得将 Session Token 写入 Electron 磁盘明文文件 -- ❌ 不得在找回账号/密码响应中区分「邮箱存在」与「邮箱不存在」(防止枚举) -- ❌ `PasswordResetToken` 不得重复使用(`is_used=True` 后立即失效) -- ❌ 登录失败响应不得区分「用户名错误」与「密码错误」 -- ❌ 不得将 `gap_x`(缺口水平位置)返回给前端(防止绕过验证) -- ❌ 耗时超过 500ms 的操作(如邮件发送)必须通过 Celery 异步执行,不得在请求线程中同步等待 +> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked. +# Fonrey 登录管理系统技术方案 + +**版本**: 2.0 | **项目**: Fonrey 房产经纪管理系统 | **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL + Redis + Celery +**关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v1.3) +**关联数据模型**: `Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md` +**最后更新**: 2026-04-25(v2.0 补充服务层设计、HTMX 交互模式、Celery 任务、错误处理规范) + +> **For AI assistants**: Read this entire file before writing any code. +> All decisions here are final. Do not suggest alternatives unless asked. + +--- + +## 一、模块定位与架构边界 + +登录管理模块(`accounts` App)负责多租户环境下的身份识别、认证、账号安全及凭据找回。 + +### 架构层级边界 + +| 层级 | 位置 | 说明 | +|------|------|------| +| Tenant ID 验证 | `shared_apps`(Public Schema) | 属于平台基础服务,在 `public` schema 下运行,无需租户切换 | +| 账号认证、找回密码等 | 租户 Schema(Tenant Schema) | 通过请求域名 `{tenant_slug}.fonrey.com` 自动切换,`django-tenants` 中间件处理 | +| Electron 客户端 | 前端 | 负责 Tenant ID 本地缓存、Session 管理、页面加载 | + +### 模块依赖关系 + +``` +accounts + ├── 依赖 → org (Staff 实名绑定,单向依赖) + ├── 依赖 → core.encryption (手机号加密) + ├── 依赖 → core.cache (Redis 工具封装) + ├── 依赖 → shared.tenants (Tenant ID 验证,Public Schema) + └── 被依赖 ← org (离职联动,通过 Service 层调用) +``` + +--- + +## 二、依赖与技术选型 + +| 依赖项 | 版本/方案 | 用途 | 说明 | +|--------|-----------|------|------| +| `django.contrib.auth` | Django 内置 | 用户认证基础框架 | 扩展 `AbstractBaseUser`,**不直接使用** `User` 模型;username 唯一性约束在租户 Schema 维度生效 | +| `django-tenants` | 已有 | 多租户隔离 | `UserAccount` 在租户 Schema;Tenant 验证接口在 `shared_apps` | +| `PostgreSQL` | 已有 | 数据持久化 | Schema 级别隔离租户数据 | +| `Redis` | 必须 | 多用途缓存 | 滑块验证 Token(TTL 3min)、登录失败计数(TTL 30min)、密码重置频率限制 | +| `Celery` | 必须 | 异步任务队列 | 邮件发送异步处理,防止登录/找回接口超时(邮件发送可能耗时 > 500ms) | +| `Pillow` | 必须(若自研验证码) | 图片处理 | 生成拼图背景图(抠出缺口)+ 拼图碎片,输出 Base64 | +| `django-ratelimit` 或自定义中间件 | 必须 | 接口限流 | Tenant 验证、登录、找回密码接口均需限流 | +| `electron-store` 或 AES 加密文件 | Electron 侧 | 本地持久化 | 加密存储 Tenant ID,不存明文;路径为 `app.getPath('userData')` | +| `secrets` (Python 标准库) | Python 内置 | Token 生成 | 使用 `secrets.token_urlsafe(64)` 生成密码重置 Token(86 字符) | + +### 滑块验证码方案选型(待确认,见开放问题) + +| 方案 | 优点 | 缺点 | +|------|------|------| +| 自研(Pillow + 前端拖拽组件) | 完全可控,无外部依赖,数据合规性好 | 需维护图库,需自己实现轨迹检测算法 | +| 第三方服务(极验 GeeTest / 网易易盾) | 开箱即用,安全性更高 | 引入外部依赖,有数据合规风险,需评估 | + +**当前方案**:暂按自研设计,后端负责人需在开发启动前确认最终选型。 + +--- + +## 三、目录结构 + +``` +fonrey/apps/ +└── accounts/ # 账号认证管理(租户级 App) + ├── models.py # UserAccount, LoginAttempt, PasswordResetToken, PasswordHistory + ├── views/ + │ ├── auth.py # 登录/登出视图(HTMX 响应) + │ ├── captcha.py # 滑块验证码视图 + │ └── recovery.py # 找回用户名/密码视图 + ├── urls.py + ├── serializers.py # API 序列化(JSON 接口,供 Electron 前端使用) + ├── forms.py # 登录表单、找回密码表单 + ├── templates/ + │ └── accounts/ + │ ├── login.html # 登录页(含滑块验证码区域) + │ ├── tenant_verify.html # Tenant 识别页(首次启动) + │ ├── change_password.html # 强制修改初始密码页 + │ ├── recover_username.html # 找回用户名页 + │ ├── recover_password.html # 找回密码(步骤 1:身份验证) + │ └── reset_password.html # 重置密码(步骤 2:设置新密码) + └── services/ + ├── auth.py # 认证逻辑:滑块验证、账号锁定、登录流程 + ├── captcha.py # 验证码生成与校验(Pillow 或第三方) + ├── recovery.py # 找回用户名/密码逻辑(含 Celery 任务触发) + ├── password.py # 密码复杂度校验、历史密码比对 + └── tenant.py # Tenant 验证逻辑(属于 shared_apps,Public Schema) + +fonrey/shared/ # Public Schema App(django-tenants shared_apps) +└── tenants/ + ├── models.py # TenantModel, Domain + └── views.py # tenant/verify/ 接口(在公共 Schema 下) +``` + +--- + +## 四、数据模型 + +> **数据模型完整定义已迁移至** `Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`,本节仅保留技术实现视角的关键说明。 + +### 4.1 表归属汇总 + +| 表名 | Schema | 说明 | +|------|--------|------| +| `user_accounts` | 租户 Schema | 账号主表,`username` 唯一性在 Schema 维度生效 | +| `login_attempts` | 租户 Schema | 登录审计,保留 ≥ 90 天 | +| `password_reset_tokens` | 租户 Schema | 一次性重置令牌,30 分钟过期 | +| `password_histories` | 租户 Schema | 最近 3 次密码哈希,防重用 | + +### 4.2 关键约束汇总 + +- `username` 唯一性约束仅在当前租户 Schema 内生效(`django-tenants` 隔离机制),**不同租户可以有相同 username** +- 密码存储使用 Django 默认 `PBKDF2+SHA256`(`make_password`),**后端不得明文存储或传输** +- `phone_enc` 字段使用 `core.encryption` AES-256-GCM 加密存储;`phone_hash` SHA-256 哈希用于唯一性校验 +- `locked_until` 字段持久化锁定到期时间,防止 Redis 故障导致锁定状态丢失 + +--- + +## 五、服务层设计(Service Layer) + +### 5.1 `services/auth.py` — 核心认证服务 + +```python +# apps/accounts/services/auth.py + +class AuthService: + + LOGIN_FAIL_LIMIT = 5 # 连续失败次数触发锁定 + LOCK_DURATION_MINUTES = 30 # 锁定时长(分钟) + + @classmethod + def authenticate(cls, username: str, password: str, captcha_pass_token: str, + tenant_id: str, ip_address: str, user_agent: str) -> dict: + """ + 完整登录流程: + 1. 校验 captcha_pass_token(一次性凭证,Redis 查询后立即删除) + 2. 查询账号(不存在则记录审计日志,返回通用错误) + 3. 检查账号状态(locked / disabled) + 4. 校验密码 + 5. 登录成功后:更新 last_login,清零失败计数,返回账号信息 + 6. 失败时:递增失败计数,超限触发锁定 + + Returns: + {'success': True, 'user': UserAccount, 'is_initial_password': bool} + {'success': False, 'error_code': str, 'error_message': str} + """ + ... + + @classmethod + def _check_lock_status(cls, user: 'UserAccount') -> bool: + """检查账号锁定状态,自动解锁已到期的锁定""" + ... + + @classmethod + def _increment_fail_count(cls, tenant_id: str, username: str) -> int: + """递增失败计数,返回当前计数;超限时触发账号锁定""" + ... + + @classmethod + def _trigger_lock(cls, user: 'UserAccount') -> None: + """触发账号锁定:status=locked, locked_until=now+30min""" + ... + + @classmethod + def unlock_account(cls, user: 'UserAccount') -> None: + """管理员手动解锁账号""" + ... +``` + +### 5.2 `services/captcha.py` — 验证码服务 + +```python +# apps/accounts/services/captcha.py + +class CaptchaService: + + CAPTCHA_TTL_SECONDS = 180 # 验证会话有效期(3分钟) + PASS_TOKEN_TTL_SECONDS = 180 # 通过凭证有效期(3分钟) + + @classmethod + def generate(cls) -> dict: + """ + 生成滑块拼图验证码。 + Returns: + { + 'session_token': str, # Redis Key uuid,供前端提交时携带 + 'background_b64': str, # 背景图(含缺口)Base64 + 'puzzle_b64': str, # 拼图碎片 Base64 + 'gap_y': int, # 缺口 Y 坐标(前端定位碎片初始位置) + } + 注意:缺口 X 坐标(gap_x)不返回给前端,服务端保存在 Redis。 + """ + ... + + @classmethod + def verify(cls, session_token: str, slide_x: int, trajectory: list) -> dict: + """ + 校验滑动结果。 + Args: + session_token: generate() 返回的会话标识 + slide_x: 用户最终滑动距离(px) + trajectory: 滑动轨迹,格式 [{'x': int, 'y': int, 't': int}, ...] + Returns: + {'pass': True, 'pass_token': str} # 通过,pass_token 用于登录接口 + {'pass': False, 'message': str} # 失败,前端自动刷新拼图 + 校验规则: + 1. 位置偏差:abs(slide_x - gap_x) <= 5px + 2. 轨迹特征:存在加速→减速曲线,拒绝匀速/程序化轨迹 + """ + ... +``` + +### 5.3 `services/recovery.py` — 找回账号服务 + +```python +# apps/accounts/services/recovery.py + +class RecoveryService: + + RESET_LINK_EXPIRE_MINUTES = 30 + MAX_EMAILS_PER_HOUR = 3 + + @classmethod + def request_username_recovery(cls, email: str) -> None: + """ + 发起找回用户名。 + - 无论邮箱是否存在,统一返回「如该邮箱已绑定账号,您将收到邮件」 + - 邮箱存在时:触发 Celery 任务异步发送邮件 + - 限频:同一邮箱 1 小时内最多 3 次(Redis 计数) + """ + ... + + @classmethod + def request_password_reset(cls, username: str, email: str) -> None: + """ + 发起找回密码(步骤 1)。 + - 无论匹配结果,统一返回「如信息匹配,重置链接将发送至邮箱」(防枚举) + - 匹配成功时:生成 PasswordResetToken,触发 Celery 异步发送邮件 + - 限频:同一账号 1 小时内最多 3 次(Redis 计数) + """ + ... + + @classmethod + def reset_password(cls, token_str: str, new_password: str) -> dict: + """ + 重置密码(步骤 2)。 + Returns: + {'success': True} + {'success': False, 'error_code': 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'PASSWORD_REUSED'} + 操作顺序: + 1. 查询并校验 token(is_used=False, expires_at > now) + 2. 校验密码复杂度 + 3. 校验历史密码(最近 3 次) + 4. 更新密码哈希,is_initial_password=False + 5. 标记 token is_used=True + 6. 清除该账号所有有效 Session(强制重新登录) + 7. 写入 PasswordHistory + """ + ... +``` + +### 5.4 `services/password.py` — 密码规则服务 + +```python +# apps/accounts/services/password.py + +class PasswordService: + + MIN_LENGTH = 8 + MAX_LENGTH = 32 + HISTORY_COUNT = 3 # 保留最近 N 条历史密码 + + @classmethod + def validate_complexity(cls, password: str) -> list[str]: + """ + 校验密码复杂度。 + Returns: 错误列表(空列表表示通过) + 规则: + - 长度 8~32 位 + - 必须包含字母(区分大小写) + - 必须包含数字 + """ + ... + + @classmethod + def check_history(cls, user: 'UserAccount', new_password: str) -> bool: + """ + 检查新密码是否与最近 3 次历史密码重复。 + Returns: True(允许使用)/ False(与历史重复) + """ + ... + + @classmethod + def save_history(cls, user: 'UserAccount', password_hash: str) -> None: + """ + 保存新密码哈希至历史记录,超出 HISTORY_COUNT 时删除最旧记录。 + """ + ... +``` + +--- + +## 六、Celery 异步任务 + +```python +# apps/accounts/tasks.py + +from celery import shared_task + +@shared_task( + name='accounts.send_username_recovery_email', + max_retries=3, + default_retry_delay=60, # 失败后 60 秒重试 +) +def send_username_recovery_email(email: str, username: str, company_name: str) -> None: + """ + 发送找回用户名邮件。 + 失败时自动重试最多 3 次;3 次均失败则写入告警日志(Sentry)。 + 邮件内容:用户名 + 发送时间 + 联系管理员说明。 + """ + ... + + +@shared_task( + name='accounts.send_password_reset_email', + max_retries=3, + default_retry_delay=60, +) +def send_password_reset_email(email: str, reset_link: str, company_name: str, + expires_at: str) -> None: + """ + 发送密码重置链接邮件。 + 失败时自动重试最多 3 次;3 次均失败则写入告警日志(Sentry)。 + 邮件内容:重置链接(30分钟有效)+ 安全说明。 + """ + ... +``` + +> **重试策略**:邮件发送失败时不向前端返回错误(用户已看到「邮件已发送」提示),在后台静默重试;3 次重试均失败后通过 Sentry 上报告警,管理员可在后台查看 Token 手动告知用户。 + +--- + +## 七、接口清单 + +| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 限流规则 | 响应格式 | 说明 | +|------|------|------------|------------|---------|---------|------| +| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | 每 IP 每分钟 ≤ 10 次 | JSON | Tenant ID 验证 | +| `/api/auth/captcha/` | GET | Tenant | 否 | — | JSON | 获取滑块拼图验证码 | +| `/api/auth/captcha/verify/` | POST | Tenant | 否 | — | JSON | 提交滑动轨迹,返回一次性通过凭证 | +| `/api/auth/login/` | POST | Tenant | 否 | 每 IP 每分钟 ≤ 20 次 | JSON | 账号密码登录 | +| `/api/auth/logout/` | POST | Tenant | 是 | — | JSON | 登出,使服务端 Session 失效 | +| `/api/auth/password/change/` | POST | Tenant | 是 | — | JSON / HTMX | 强制修改初始密码(登录后跳转) | +| `/api/auth/recover/username/` | POST | Tenant | 否 | 每邮箱每小时 ≤ 3 次 | JSON / HTMX | 发起找回用户名(发送邮件) | +| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 每账号每小时 ≤ 3 次 | JSON / HTMX | 发起找回密码(发送重置链接邮件) | +| `/api/auth/recover/password/reset/` | POST | Tenant | 否(Token 鉴权) | — | JSON / HTMX | 提交新密码,使用 PasswordResetToken 校验 | +| `/api/auth/login/phone/` | POST | Tenant | 否 | — | JSON | **预留**,v2 实现,手机验证码登录 | +| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | — | JSON | **预留**,v2 实现,获取微信二维码 | +| `/api/auth/wechat/callback/` | POST | Tenant | 否 | — | JSON | **预留**,v2 实现,微信扫码回调 | + +### 7.1 Tenant 验证接口规范 + +``` +POST /api/auth/tenant/verify/ + +Request Body: +{ + "tenant_id": "202500010001" // 固定 12 位纯数字 +} + +Response 200 (验证通过): +{ + "valid": true, + "tenant_name": "XX房产经纪有限公司", + "tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png", + "login_url": "https://xxx.fonrey.com/auth/login/" +} + +Response 200 (验证失败): +{ + "valid": false, + "error_code": "TENANT_NOT_FOUND", + "message": "识别码无效" +} +``` + +> 失败响应统一返回 HTTP 200,不区分「未找到」与「已禁用」,防止枚举攻击。 + +### 7.2 登录接口规范 + +``` +POST /api/auth/login/ + +Request Body: +{ + "username": "string", + "password": "string", + "captcha_pass_token": "string" // 滑块验证通过后的一次性凭证(UUID) +} + +Response 200 (登录成功): +{ + "success": true, + "token": "...", + "user": { + "id": 1, + "username": "...", + "display_name": "...", + "is_initial_password": false + } +} + +Response 200 (登录失败): +{ + "success": false, + "error_code": "WRONG_CREDENTIALS" | "ACCOUNT_LOCKED" | "ACCOUNT_DISABLED" | "CAPTCHA_INVALID", + "message": "...", + "lock_remaining_seconds": 1800 // 仅 ACCOUNT_LOCKED 时返回 +} +``` + +> **注意**:`WRONG_CREDENTIALS` 不区分「用户名错误」与「密码错误」,防止枚举攻击。 + +### 7.3 验证码接口规范 + +``` +GET /api/auth/captcha/ + +Response 200: +{ + "session_token": "uuid-string", // 提交验证时携带 + "background_b64": "data:image/png;base64,...", // 带缺口的背景图 + "puzzle_b64": "data:image/png;base64,...", // 拼图碎片 + "gap_y": 120, // 缺口 Y 坐标(用于定位碎片初始位置) + "width": 320, // 背景图宽度(px) + "height": 160 // 背景图高度(px) +} + +POST /api/auth/captcha/verify/ + +Request Body: +{ + "session_token": "uuid-string", + "slide_x": 185, // 最终滑动距离(px) + "trajectory": [ + {"x": 0, "y": 0, "t": 0}, + {"x": 20, "y": 1, "t": 80}, + {"x": 185, "y": 2, "t": 1200} + ] +} + +Response 200 (验证通过): +{ + "pass": true, + "pass_token": "uuid-string" // 一次性凭证,TTL 3分钟,登录时携带 +} + +Response 200 (验证失败): +{ + "pass": false, + "message": "验证失败,请重新拖动" +} +``` + +--- + +## 八、前端交互模式(HTMX + Alpine.js) + +### 8.1 页面结构说明 + +登录相关页面均为**全页面渲染**(Server-Side Rendered),Electron 客户端通过 `BrowserWindow.loadURL()` 加载完整 HTML。登录流程中的局部交互(如验证码刷新、错误提示)通过 HTMX 局部刷新实现。 + +### 8.2 登录页核心交互 + +```html + + + +
+ +
+ 验证图片 +
+
+
+ + +
+
+ 拖动完成拼图 + 验证通过 ✓ +
+
+ + + +
+ + +
+ + + + +
+
+
登录中...
+``` + +> **Alpine.js 职责**:管理验证码状态(加载中/通过/失败)、滑动轨迹记录、`pass_token` 绑定到表单隐藏字段。 +> **HTMX 职责**:表单提交、错误反馈局部渲染(`hx-target="#login-feedback"`)。 + +### 8.3 HTMX 响应片段规范 + +登录接口在 HTMX 请求时(`HX-Request: true` Header)返回 HTML 片段而非 JSON,供 `hx-target` 局部替换: + +**登录成功**(服务端返回 302 重定向,HTMX 通过 `HX-Redirect` Header 处理): +```python +# views/auth.py +if request.headers.get('HX-Request'): + response = HttpResponse() + response['HX-Redirect'] = '/dashboard/' # 跳转首页 + return response +``` + +**登录失败**(返回错误提示 HTML 片段): +```html + +
+ 用户名或密码错误,请重新输入 +
+``` + +**初始密码状态**(登录成功但需修改密码): +```python +if request.headers.get('HX-Request'): + response = HttpResponse() + response['HX-Redirect'] = '/auth/password/change/' + return response +``` + +--- + +## 九、Redis Key 规范 + +| 用途 | Key 格式 | 类型 | TTL | 说明 | +|------|----------|------|-----|------| +| 滑块验证会话(含缺口位置) | `captcha_session:{uuid}` | HASH | 3 分钟 | 存储 `gap_x`, `session_token`;验证后立即删除 | +| 滑块验证通过凭证 | `captcha_pass:{uuid}` | STRING | 3 分钟 | 登录接口验证后立即删除(单次有效) | +| 登录失败计数 | `login_fail:{tenant_id}:{username}` | STRING | 30 分钟 | 计数 ≥ 5 时触发锁定;TTL 30 分钟自动清零 | +| 找回邮件发送频率 | `recover_email:{email}` | STRING | 1 小时 | 记录已发送次数,上限 3 次/小时 | +| 密码重置 Token 生成频率 | `recover_reset:{user_id}` | STRING | 1 小时 | 同一账号生成次数,上限 3 次/小时 | +| Tenant ID 限流 | `tenant_verify_ip:{ip}` | STRING | 1 分钟 | 计数 ≥ 10 时拒绝请求 | + +> **故障恢复**:Redis 重启后,登录失败计数归零(用户可正常登录);账号锁定状态由 `user_accounts.locked_until` 持久化保证,不依赖 Redis。 + +--- + +## 十、安全机制设计 + +### 10.1 滑块拼图验证码 + +- **图片生成**:`Pillow` 从预置图库随机抽取背景图,服务端随机生成缺口位置,抠出缺口并生成拼图碎片,两者分别以 Base64 返回前端 +- **缺口位置保护**:`gap_x`(水平位置)仅存于服务端 Redis,**不返回给前端**;前端通过 `slide_x` 提交,服务端对比 `gap_x` 校验 +- **轨迹校验**(双重判断): + - **位置偏差**:`abs(slide_x - gap_x) ≤ 5px` + - **轨迹特征**:速度变化曲线存在加速→减速(人类滑动特征),拒绝匀速/程序化轨迹 +- **独立计数**:验证码失败**不计入**账号密码错误次数,两者独立计数 +- **单次有效**:`captcha_pass_token` TTL 3 分钟,登录接口校验后立即删除 + +### 10.2 账号锁定机制 + +``` +同一账号连续密码错误 ≥ 5 次: + 1. Redis `login_fail:{tenant_id}:{username}` 计数达到阈值 + 2. 更新 user_accounts.status = 'locked' + 3. 设置 user_accounts.locked_until = now() + 30min + 4. 锁定状态下,登录接口直接返回 ACCOUNT_LOCKED,不再校验密码 + +解锁条件(任一满足): + A. locked_until 到期:应用层在下次登录时检测,自动恢复 status=active + B. Tenant Admin 手动解锁:调用 AuthService.unlock_account() +``` + +### 10.3 密码安全 + +| 规则 | 说明 | +|------|------| +| 存储哈希 | Django `PBKDF2+SHA256`(`make_password`) | +| 传输安全 | 强制 HTTPS,前端**不加密**密码(HTTPS 层保证) | +| 复杂度 | 长度 8~32 位,必须包含字母(区分大小写)+ 数字;建议特殊符号(非强制) | +| 历史密码 | 不得与最近 3 次历史密码哈希相同(含系统固定初始密码) | +| Session 有效期 | 默认 8 小时;可由 Tenant Admin 在「系统设置」中调整 | + +### 10.4 防枚举攻击设计 + +| 场景 | 防御措施 | +|------|---------| +| 登录失败 | 不区分「用户名错误」与「密码错误」,统一返回 `WRONG_CREDENTIALS` | +| 找回用户名/密码 | 无论邮箱/用户名是否存在,统一返回相同响应文案 | +| Tenant ID 验证 | 不区分「租户不存在」与「租户已禁用」;IP 限流每分钟 ≤ 10 次 | +| 密码重置 Token | Token 使用 `secrets.token_urlsafe(64)` 生成(86 字符),不可预测 | + +### 10.5 密码重置流程安全要点 + +- Token 由 `secrets.token_urlsafe(64)` 生成,86 字符,全局唯一 +- 单次有效:使用后立即标记 `is_used=True`(先标记再执行,防止并发重放) +- 有效期 30 分钟(`expires_at = created_at + timedelta(minutes=30)`) +- 重置成功后:清除该账号所有有效 Session(强制重新登录) +- 重置成功后:`is_initial_password = False`,写入 `PasswordHistory` + +--- + +## 十一、Electron 客户端约定 + +| 约定项 | 规格 | +|--------|------| +| Tenant ID 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密文件,**不存明文** | +| Session Token 存储 | 内存(`global` 变量)+ Chromium `session` Cookie,**不写入磁盘明文文件** | +| 登录页加载方式 | 主进程根据 Tenant ID 构建 `https://{tenant_slug}.fonrey.com/auth/login/`,通过 `BrowserWindow.loadURL()` 加载 | +| 多标签页 | 同一 `BrowserWindow` 内所有页面共享同一 Session Cookie | +| 客户端登出 | 调用 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie | +| 窗口关闭 | Session 保留(不自动登出),下次打开若 Session 未过期则直接进入系统 | +| 强制更新 | 客户端版本低于服务端 `min_required_version` 时,阻断登录流程,展示更新提示(详见发布管理模块 PRD) | +| Tenant ID 缓存校验 | 非首次启动时,客户端向服务端发起缓存 Tenant ID 有效性校验(`POST /api/auth/tenant/verify/`);无效则清除缓存,重新显示 Tenant 识别界面 | + +--- + +## 十二、多租户隔离要点 + +- `UserAccount`、`LoginAttempt`、`PasswordResetToken`、`PasswordHistory` 均位于**租户 Schema 内**,数据完全隔离 +- `username` 唯一性约束在 Schema 维度生效,不同租户可以存在相同 username +- Tenant 验证接口(`/api/auth/tenant/verify/`)位于 **Public Schema**(`shared_apps`),查询 `TenantModel` +- 登录等接口通过请求域名(`{tenant_slug}.fonrey.com`)自动切换 Schema,由 `django-tenants` 中间件处理,**无需手动切换** +- 所有接口禁止跨租户数据访问,ORM 查询范围自动限制在当前 Schema + +--- + +## 十三、错误处理规范 + +### 13.1 标准错误码 + +| Error Code | HTTP Status | 含义 | 前端显示文案 | +|------------|-------------|------|-------------| +| `WRONG_CREDENTIALS` | 200 | 用户名或密码错误 | 「用户名或密码错误,请重新输入」 | +| `ACCOUNT_LOCKED` | 200 | 账号已锁定 | 「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」 | +| `ACCOUNT_DISABLED` | 200 | 账号已停用 | 「账号已停用,请联系您的管理员」 | +| `CAPTCHA_INVALID` | 200 | 验证码凭证无效/已过期 | 「验证码已失效,请重新验证」 | +| `CAPTCHA_FAIL` | 200 | 滑块位置/轨迹校验失败 | 「验证失败,请重新拖动」 | +| `TENANT_NOT_FOUND` | 200 | Tenant ID 无效 | 「识别码无效,请联系您的系统管理员获取正确的识别码」 | +| `TOKEN_INVALID` | 200 | 重置 Token 无效或已使用 | 「链接已过期或已使用,请重新申请」 | +| `TOKEN_EXPIRED` | 200 | 重置 Token 已过期 | 「链接已过期,请重新申请」 | +| `PASSWORD_TOO_WEAK` | 200 | 密码不符合复杂度 | 逐条显示不满足的规则 | +| `PASSWORD_REUSED` | 200 | 密码与历史密码相同 | 「新密码不能与最近 3 次历史密码相同」 | + +> **设计原则**:所有登录相关接口统一返回 HTTP 200,通过 `error_code` 字段区分业务错误,避免 HTTP 状态码暴露系统行为(防止通过 4xx/5xx 枚举账号状态)。 + +### 13.2 异常监控 + +- 所有未预期异常(5xx)通过 Sentry 上报,含 `tenant_id`、`username`(脱敏)、堆栈信息 +- 邮件发送 Celery 任务 3 次重试失败后,上报 Sentry 告警并记录 `task_id`,管理员可在系统后台查询 + +--- + +## 十四、已知风险与缓解措施 + +| 风险 | 可能性 | 影响 | 缓解措施 | +|------|--------|------|---------| +| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征,拒绝匀速/程序化轨迹;后续可引入设备指纹 | +| Tenant ID 枚举攻击 | 低 | 中 | 限流(每 IP 每分钟 ≤ 10 次);响应不区分「未找到」与「已禁用」 | +| 密码重置 Token 泄露 | 低 | 高 | 单次有效 + 30 分钟过期 + HTTPS 传输 | +| 邮件发送失败 | 中 | 中 | 异步任务自动重试 3 次;失败写入 Sentry 告警;管理员可通过后台查看 Token 手动告知用户 | +| 多端并发登录 | 高(正常场景) | 低 | 本期允许;v2 可在 Token 引入版本号实现踢出策略 | +| Redis 故障导致锁定状态丢失 | 低 | 中 | `locked_until` 字段持久化至 PostgreSQL,Redis 故障不影响锁定判断 | + +--- + +## 十五、开放问题(开发启动前必须确认) + +| # | 问题 | 负责人 | 截止 | +|---|------|--------|------| +| 1 | 邮件服务商选型:SendGrid / 阿里云邮件推送 / SMTP 自建? | 后端负责人 + 运维 | 开发启动前 | +| 2 | 滑块验证码方案:自研(Pillow)还是第三方(极验 / 网易易盾)? | 后端负责人 + 安全 | 开发启动前 | +| 3 | Session 有效期默认值 8 小时,是否允许 Tenant Admin 自行配置? | 产品经理 | 开发启动前 | +| 4 | 账号锁定后是否自动发邮件通知用户和/或管理员? | 产品经理 | 开发启动前 | +| 5 | 历史密码校验范围:最近 3 次是否足够?是否增加「不得与用户名相同」规则? | 产品经理 | 开发启动前 | + +--- + +## 十六、明确禁止 + +- ❌ 不得使用 Django 原生 `User` 模型,必须扩展 `AbstractBaseUser` +- ❌ 不得在全局 Schema 创建 `UserAccount` 表(必须在租户 Schema 内) +- ❌ 不得明文存储或传输密码 +- ❌ 不得在 `LoginAttempt` 记录中存储密码明文(含错误密码) +- ❌ 不得在前端做密码哈希(HTTPS 层保证传输安全) +- ❌ 不得将 Session Token 写入 Electron 磁盘明文文件 +- ❌ 不得在找回账号/密码响应中区分「邮箱存在」与「邮箱不存在」(防止枚举) +- ❌ `PasswordResetToken` 不得重复使用(`is_used=True` 后立即失效) +- ❌ 登录失败响应不得区分「用户名错误」与「密码错误」 +- ❌ 不得将 `gap_x`(缺口水平位置)返回给前端(防止绕过验证) +- ❌ 耗时超过 500ms 的操作(如邮件发送)必须通过 Celery 异步执行,不得在请求线程中同步等待 diff --git a/Project/fonrey/preview.html b/Project/fonrey/UI_DESIGN/preview.html similarity index 100% rename from Project/fonrey/preview.html rename to Project/fonrey/UI_DESIGN/preview.html diff --git a/Project/fonrey/UI_DESIGN/客源列表_UI.html b/Project/fonrey/UI_DESIGN/客源列表_UI.html index e1660329..fe2653d9 100644 --- a/Project/fonrey/UI_DESIGN/客源列表_UI.html +++ b/Project/fonrey/UI_DESIGN/客源列表_UI.html @@ -873,17 +873,17 @@ - -
+ +
+
-
- +