Compare commits
233 Commits
5ee507c33a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 454e71fb3f | |||
| 91f9e29167 | |||
| 992fb88ff4 | |||
| b6e3e859e1 | |||
| c4f76d9c2a | |||
| 4141e18896 | |||
| 836bbdd90d | |||
| b147c5730f | |||
| b305f2ee0c | |||
| 06dde3173a | |||
| e01ca79bdc | |||
| 7b91c310eb | |||
| 369c9e90ea | |||
| 17e67f7bd6 | |||
| b40abbcd47 | |||
| 111bc65b7b | |||
|
|
90f3811b83 | ||
|
|
ca33cc141f | ||
|
|
464c5fce51 | ||
| 804954c367 | |||
| 8525e8e6da | |||
| f96c445cec | |||
|
|
b7469954c5 | ||
|
|
94d8061eb5 | ||
|
|
57600598ac | ||
|
|
4030a91100 | ||
|
|
1da12a27f0 | ||
|
|
91fc0b06d6 | ||
|
|
eada3af824 | ||
| c961c6a394 | |||
|
|
b2aadf771a | ||
| c3f9de5f9f | |||
| 070bd42886 | |||
|
|
15cd44b2ca | ||
|
|
faf3aa51bb | ||
| eedfafcae2 | |||
| 2c56d5a031 | |||
| 74d02d0df2 | |||
| 0e548ce5dc | |||
| f71229f0c3 | |||
| c51cc4c58b | |||
|
|
e4cf7f8485 | ||
| 365caa800a | |||
| f8b421ece6 | |||
| c898cc3fb9 | |||
|
|
cf4001c4f8 | ||
| 3224ec4787 | |||
| b83b4e3105 | |||
| 0d764a0c4a | |||
| b574c99af6 | |||
| de7ebe9256 | |||
|
|
5854781fa8 | ||
| dfcf7de003 | |||
|
|
712a33fbac | ||
| 4422c0eac8 | |||
| 83c6e24e7c | |||
| fbd6107be4 | |||
|
|
1c7c7d673e | ||
| 1642757cd0 | |||
| 23bef113dd | |||
| 997e25aae6 | |||
|
|
6dc50b68a0 | ||
| b7d9d0f5d1 | |||
|
|
d42bc16120 | ||
| d2ae5b3948 | |||
|
|
1abf0d56f5 | ||
|
|
6270ba56ee | ||
| ecdf295ded | |||
|
|
f09834b5a5 | ||
| 191797c01b | |||
| c073392db8 | |||
| 22f77e0660 | |||
| f1de106298 | |||
| 8cceccf489 | |||
| 82741b1c69 | |||
| c304b2b2e2 | |||
| 52337cf662 | |||
| c5a6f4d76e | |||
| 1e8673c5dd | |||
| 5c6911b44d | |||
| a6a0d4435c | |||
| ec3be78d36 | |||
| 8c909c9c08 | |||
| 2613a74c73 | |||
| 7cb331a532 | |||
| 6aa1571161 | |||
| bd2a4c5331 | |||
| 9fccf27053 | |||
| e812681628 | |||
| f7cd041319 | |||
| 158c43e1b1 | |||
| c83d6fbae6 | |||
| 31fc369fd5 | |||
| 55d3745bb0 | |||
| ac7fdfc316 | |||
| 36651b38a9 | |||
| 466273a164 | |||
| 480d64ae81 | |||
| 6006601b6b | |||
| 77fae85f60 | |||
| df840cc5b8 | |||
| 7d0e10b299 | |||
| 3b6697df35 | |||
| aa980f8da2 | |||
| fdb657965e | |||
| 84d27f4210 | |||
| 20f686ea5f | |||
| a26d62bb6d | |||
| ef8474f0d2 | |||
| 432174c5e3 | |||
| d54fdb2d26 | |||
| 31d316b096 | |||
| 4b6b2f970c | |||
| e677a87510 | |||
| 33afef323c | |||
| 7903d703b9 | |||
| e4f6f463cb | |||
| cc23df1883 | |||
| 3148216d38 | |||
| 207d6e8b42 | |||
| 0d6f30a55a | |||
| 6bd1759da8 | |||
| cac9d11fef | |||
| 2e0b9940ed | |||
| 81d97ce6c1 | |||
| f7e0d2b400 | |||
| 75b9e25e68 | |||
| 7550b4ee18 | |||
| 4c2ec85278 | |||
| 160a15c1ad | |||
| 1ad4e6dcf5 | |||
| 3b55f3af4d | |||
| 761fa71f69 | |||
| 2db051a399 | |||
| 5cf21b65ee | |||
| 989ec86c77 | |||
| ca96e409be | |||
| 171d4b5d3e | |||
| 688a996082 | |||
| 756b30e188 | |||
| 647d446780 | |||
| 7cecf10c79 | |||
| cc8ebc60e3 | |||
| a96baa8fb7 | |||
| 4e9ee6f51e | |||
| bea2c71242 | |||
| 2d82830d47 | |||
| b598057f03 | |||
| 9de4d0a9b4 | |||
| e907ba8c5f | |||
| 782df914d9 | |||
| a295b739a0 | |||
| feea929082 | |||
| fcfe9c7ae5 | |||
| 980a3e89bf | |||
| e235a30768 | |||
| 28dbb1a23a | |||
| e656c04794 | |||
| c59cc07327 | |||
| 6a8362bb5a | |||
| 059fffb9c2 | |||
| 4e43ae0ed3 | |||
| 0179a6532c | |||
| 42961b7e63 | |||
| 4652411bf2 | |||
| cb6ec38943 | |||
| b15569f319 | |||
| 73c54c1c41 | |||
| e4463f12e1 | |||
| a66a882a41 | |||
| ec58afd9f0 | |||
| 7e86320c6d | |||
| 7742098715 | |||
| 2971706866 | |||
| 5c5732418b | |||
| 5a63b6dc72 | |||
| b91831a5fd | |||
| c8599198a0 | |||
| e462f96d61 | |||
| b0cdd19bfc | |||
| 6f44ff76a2 | |||
| d1e7e4344b | |||
| e823c78a9b | |||
| 377d32cd39 | |||
| fd3f24ba27 | |||
| 28830a5393 | |||
| 201e165f36 | |||
| 087e05cf73 | |||
| e8ad058cdd | |||
| fa0a6fa92c | |||
| f2c14bcce1 | |||
| 3d9d5c8ca7 | |||
| 772cbf2051 | |||
| 72f3673978 | |||
| b1e6af2458 | |||
| 143d1fd105 | |||
| de096f2f88 | |||
| 24218550d2 | |||
| c4a04cbcee | |||
| 0fe7ba237f | |||
| 914c8f6925 | |||
| b3b6be6114 | |||
| ca7a910543 | |||
| 8df9990f15 | |||
| 4b4ffcd0c2 | |||
| 0714b37c4d | |||
| ac524d1ff5 | |||
| cb7c11e14f | |||
| 177469a1cd | |||
| 194edff100 | |||
| 9b4c1e33d9 | |||
| af7f28a13b | |||
| d55e364abc | |||
| 22f148e5ec | |||
| d46e212d5b | |||
| 3fde66194b | |||
| 20b560fef4 | |||
| 463ae32c13 | |||
| 05698f00ea | |||
| d0357ebc63 | |||
| 9b5a9a9902 | |||
| 08da9a4d38 | |||
| 098be11603 | |||
| 1e4716618d | |||
| 31c2abdd2f | |||
| 74e69a974d | |||
| 5d18f9d537 | |||
| 455f7a3c40 | |||
| d7bf4ae6de | |||
| 31565fe752 | |||
| 6ab2838935 | |||
| 8341ee6cc4 | |||
| fc0dde291f |
3
.gitignore
vendored
@@ -1 +1,4 @@
|
|||||||
.obsidian/
|
.obsidian/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
|||||||
6
.opencode/opencode.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"plugin": [
|
||||||
|
"oh-my-openagent"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 830 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 712 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 174 KiB |
BIN
Asset/Attachment/ishenwei/慧世睿联/营业执照/IMG-20260617094822192.jpg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 553 KiB |
@@ -667,3 +667,8 @@ Please consult [these instructions](https://github.com/ChromeDevTools/chrome-dev
|
|||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
See [Troubleshooting](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/troubleshooting.md).
|
See [Troubleshooting](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/troubleshooting.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infographic
|
||||||
|

|
||||||
114
Hermes/xingzhi/Hermes自定义技能说明.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Hermes 自定义技能说明
|
||||||
|
|
||||||
|
> 创建日期:2026-04-20
|
||||||
|
> 更新日期:2026-04-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技能总览
|
||||||
|
|
||||||
|
| 技能名 | 用途 | 调用方式 |
|
||||||
|
| --------------------------------- | --------------------------------------------- | ------------- |
|
||||||
|
| `blogwatcher-daily` | RSS 订阅监控 + 每日笔记生成 | cronjob |
|
||||||
|
| `claude-code-executor` | 用 TMUX 启动 Claude Code 并委托任务 | delegate_task |
|
||||||
|
| `claude-code-infographic-prompts` | 调用 Claude Code 的 baoyu-infographic 生成信息图提示词 | 手动调用 |
|
||||||
|
| `marker-pdf-to-markdown` | PDF 转 Markdown(marker_single 单文件 / marker 批量) | terminal |
|
||||||
|
| `whisper-audio-to-text` | 音频/视频转文字(Whisper) | terminal |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. claude-code-executor
|
||||||
|
|
||||||
|
**路径**:`~/.hermes/skills/custom/claude-code-executor/SKILL.md`
|
||||||
|
|
||||||
|
**用途**:通过 TMUX 启动 Claude Code(bypassPermissions 模式),发送任务指令,监控完成状态。
|
||||||
|
|
||||||
|
**触发关键词**:用户说"请用 Claude Code 做 xxx"、"用 Claude Code 执行 xxx"
|
||||||
|
|
||||||
|
**核心流程**:
|
||||||
|
1. 启动 TMUX session `claude-task`
|
||||||
|
2. 等 8 秒后发送 Enter(bypassPermissions 确认)
|
||||||
|
3. 发送完整任务指令
|
||||||
|
4. 监控 `done:` 输出
|
||||||
|
5. 清理 tmux session
|
||||||
|
|
||||||
|
**SSH 到 ubuntu2 命令**:`ssh ubuntu2`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. claude-code-infographic-build
|
||||||
|
|
||||||
|
**路径**:`~/.hermes/skills/custom/claude-code-infographic-build/SKILL.md`
|
||||||
|
|
||||||
|
**用途**:调用 Claude Code 的 `baoyu-infographic` 技能,为笔记内容生成信息图提示词,并进一步生成图片添加到原始笔记。
|
||||||
|
|
||||||
|
**完整三步流程**:
|
||||||
|
1. **步骤①**:baoyu-infographic 生成三部分提示词 → 保存到 Hermes/xingzhi/
|
||||||
|
2. **步骤②**:在同一 Claude Code session 中,baoyu-imagine 生成图片 → 保存到 Hermes/xingzhi/
|
||||||
|
3. **步骤③**:obsidian skill 将图片引用添加到原始笔记
|
||||||
|
|
||||||
|
**三部分提示词格式**:
|
||||||
|
1. **系统提示词** — Image Specifications + Core Principles
|
||||||
|
2. **风格锁定提示词** — Layout Guidelines + Style Guidelines
|
||||||
|
3. **内容结构提示词** — Text Requirements + 具体内容
|
||||||
|
|
||||||
|
**布局参考**(`~/.claude/skills/baoyu-infographic/references/layouts/`):
|
||||||
|
- `hub-spoke` — 适合 mind-map(标注了 "Best For: Mind maps")
|
||||||
|
- `circular-flow` — 适合循环流程
|
||||||
|
- `venn` — 适合交集关系
|
||||||
|
|
||||||
|
**风格参考**(`~/.claude/skills/baoyu-infographic/references/styles/`):
|
||||||
|
- `corporate-memphis` — 商务孟菲斯风格
|
||||||
|
- `chalkboard` — 粉笔黑板风格
|
||||||
|
- `cyberpunk-neon` — 赛博霓虹风格
|
||||||
|
- `warm` / `notion` / `minimal` / `blueprint` / `watercolor` / `elegant`
|
||||||
|
|
||||||
|
**文件命名**:`[主题]-infographic-prompts-YYYY-MM-DD.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. blogwatcher-daily
|
||||||
|
|
||||||
|
**路径**:`~/.hermes/skills/custom/blogwatcher-daily/SKILL.md`
|
||||||
|
|
||||||
|
**用途**:RSS 订阅监控 + 每日笔记生成。使用 RSSHub + feedparser 抓取 31 个订阅,自动去重并存入 SQLite,新文章追加写入 Markdown 笔记。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. marker-pdf-to-markdown
|
||||||
|
|
||||||
|
**路径**:`~/.hermes/skills/custom/marker-pdf-to-markdown/SKILL.md`
|
||||||
|
|
||||||
|
**用途**:PDF 转 Markdown/HTML/JSON 高精度工具,支持 OCR、表格、公式识别。
|
||||||
|
|
||||||
|
**安装位置**:ubuntu2 服务器(`ssh ubuntu2`)
|
||||||
|
|
||||||
|
| 命令 | 用途 | 输入 |
|
||||||
|
|------|------|------|
|
||||||
|
| `marker_single` | 单文件转换,默认输出到 PDF 同目录 | 单个 PDF 文件 |
|
||||||
|
| `marker` | 批量转换,需文件夹输入 | 文件夹路径 |
|
||||||
|
|
||||||
|
常用选项:`--output_dir`、`--output_format json/markdown/html`、`--no-images`、`--use_llm`、`--page_range`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. whisper-audio-to-text
|
||||||
|
|
||||||
|
**路径**:`~/.hermes/skills/custom/whisper-audio-to-text/SKILL.md`
|
||||||
|
|
||||||
|
**用途**:使用 OpenAI Whisper 将音频/视频转换为文字(转录或翻译)。
|
||||||
|
|
||||||
|
**安装位置**:Mac mini(GPU 加速)、ubuntu1、ubuntu2(CPU 模式),均直接可用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关 Claude Code 技能(`~/.claude/skills/`)
|
||||||
|
|
||||||
|
| 技能名 | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `baoyu-article-illustrator` | 文章配图(分析文章结构 + 生成配图) |
|
||||||
|
| `baoyu-infographic` | 信息图生成 |
|
||||||
|
| `baoyu-cover-image` | 封面图生成 |
|
||||||
|
| `fireworks-tech-graph` | 技术图生成 |
|
||||||
|
|
||||||
|
这些 Claude Code 技能通过 `claude-code-executor` 启动 Claude Code CLI 后调用。
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Chrome DevTools MCP - Infographic Prompts
|
||||||
|
|
||||||
|
Generated: 2026-04-20
|
||||||
|
Layout: circular-flow
|
||||||
|
Style: chalkboard
|
||||||
|
Aspect: 16:9
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a professional infographic with the following specifications:
|
||||||
|
|
||||||
|
## Image Specifications
|
||||||
|
- Type: Infographic
|
||||||
|
- Layout: circular-flow (cyclic process showing continuous steps arranged in a circle)
|
||||||
|
- Style: chalkboard (chalk on black board aesthetic)
|
||||||
|
- Aspect Ratio: 16:9
|
||||||
|
- Language: English
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
- Follow the circular-flow layout structure: arrange information nodes in a circle with directional arrows
|
||||||
|
- Each section represents a tool category as a node on the circle
|
||||||
|
- Use chalk-drawn style: imperfect hand-drawn lines, chalk dust effects, white/yellow/pink/blue chalk colors
|
||||||
|
- Black chalkboard background (#1A1A1A)
|
||||||
|
- Center can hold the main concept "Chrome DevTools MCP"
|
||||||
|
- Maintain authentic chalk texture on all elements
|
||||||
|
- Use circular arrangement to show the tool workflow cycle
|
||||||
|
- Clear visual hierarchy with color variety
|
||||||
|
|
||||||
|
## Text Requirements
|
||||||
|
- Main titles prominent and readable in chalk white
|
||||||
|
- Key concepts emphasized with chalk yellow/pink
|
||||||
|
- Labels clear and appropriately sized
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Style Locking Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
## Style Guidelines - chalkboard
|
||||||
|
|
||||||
|
### Background
|
||||||
|
- Color: Chalkboard Black (#1A1A1A) or Dark Green-Black (#1C2B1C)
|
||||||
|
- Texture: Realistic chalkboard texture with subtle scratches, dust particles
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
- Hand-drawn chalk lettering with visible chalk texture
|
||||||
|
- White or bright colored chalk for emphasis
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Background: #1A1A1A (Chalkboard Black)
|
||||||
|
- Primary Text: #F5F5F5 (Chalk White)
|
||||||
|
- Accent 1: #FFE566 (Chalk Yellow) - highlights, emphasis
|
||||||
|
- Accent 2: #FF9999 (Chalk Pink) - secondary highlights
|
||||||
|
- Accent 3: #66B3FF (Chalk Blue) - links, connections
|
||||||
|
- Accent 4: #90EE90 (Chalk Green) - success indicators
|
||||||
|
|
||||||
|
### Visual Elements
|
||||||
|
- Hand-drawn chalk illustrations with sketchy, imperfect lines
|
||||||
|
- Chalk dust effects around text and key elements
|
||||||
|
- Doodles: stars, arrows, underlines, circles
|
||||||
|
- Connection lines with hand-drawn feel
|
||||||
|
- Directional arrows showing cycle flow
|
||||||
|
|
||||||
|
### Do
|
||||||
|
- Maintain authentic chalk texture on all elements
|
||||||
|
- Use imperfect, hand-drawn quality
|
||||||
|
- Add subtle chalk dust and smudge effects
|
||||||
|
- Create visual hierarchy with color variety
|
||||||
|
- Include playful doodles and annotations
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
- Use perfect geometric shapes
|
||||||
|
- Create clean digital-looking lines
|
||||||
|
- Add photorealistic elements
|
||||||
|
- Use gradients or glossy effects
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Content Structure Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
## Content Structure - circular-flow
|
||||||
|
|
||||||
|
### Center Element
|
||||||
|
- "Chrome DevTools MCP" - main concept in center of circle
|
||||||
|
|
||||||
|
### Circle Nodes (6 categories, clockwise flow):
|
||||||
|
1. INPUT AUTOMATION (9 tools)
|
||||||
|
- click, drag, fill, fill_form, handle_dialog, hover, press_key, type_text, upload_file
|
||||||
|
- Chalk Pink icon/node
|
||||||
|
|
||||||
|
2. NAVIGATION AUTOMATION (6 tools)
|
||||||
|
- close_page, list_pages, navigate_page, new_page, select_page, wait_for
|
||||||
|
- Chalk Blue icon/node
|
||||||
|
|
||||||
|
3. EMULATION (2 tools)
|
||||||
|
- emulate, resize_page
|
||||||
|
- Chalk Yellow icon/node
|
||||||
|
|
||||||
|
4. PERFORMANCE (4 tools)
|
||||||
|
- performance_analyze_insight, performance_start_trace, performance_stop_trace, take_memory_snapshot
|
||||||
|
- Chalk Green icon/node
|
||||||
|
|
||||||
|
5. NETWORK (2 tools)
|
||||||
|
- get_network_request, list_network_requests
|
||||||
|
- Chalk Pink icon/node
|
||||||
|
|
||||||
|
6. DEBUGGING (6 tools)
|
||||||
|
- evaluate_script, get_console_message, lighthouse_audit, list_console_messages, take_screenshot, take_snapshot
|
||||||
|
- Chalk Blue icon/node
|
||||||
|
|
||||||
|
### Supported Clients (around the outer edge):
|
||||||
|
- Claude Code, Cline, Cursor, VS Code, Copilot, Codex, Gemini, JetBrains, Kiro, Windsurf, Amp, Antigravity, Command Code, Factory, Mistral, OpenCode, Qoder, Warp
|
||||||
|
|
||||||
|
### Configuration Options (bottom section):
|
||||||
|
- --headless, --slim, --autoConnect, --browser-url, --channel, --viewport, --isolated, --user-data-dir
|
||||||
|
|
||||||
|
### Arrows
|
||||||
|
- Curved directional arrows connecting each node in clockwise direction
|
||||||
|
- Showing continuous workflow cycle
|
||||||
|
|
||||||
|
### Labels in English
|
||||||
|
- All text labels in English
|
||||||
|
- Use chalk white for main text, chalk yellow for emphasis
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visual Layout Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
[INPUT]
|
||||||
|
↓
|
||||||
|
↙ ↘
|
||||||
|
[DEBUGGING] ← CENTER → [NAVIGATION]
|
||||||
|
↗ ↣
|
||||||
|
↙ ↘
|
||||||
|
[NETWORK] ←─────────────────────→ [EMULATION]
|
||||||
|
↘ ↙
|
||||||
|
↗ ↣
|
||||||
|
[PERFORMANCE] →
|
||||||
|
↓
|
||||||
|
[???]
|
||||||
|
|
||||||
|
Actually circular flow (clockwise):
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ [EMULATION] → [PERFORMANCE] │
|
||||||
|
│ ↗ ↗ │
|
||||||
|
│ │ │ │
|
||||||
|
│ [DEBUGGING] [NAVIGATION] │
|
||||||
|
│ │ │ │
|
||||||
|
│ ↖ ↘ │
|
||||||
|
│ [NETWORK] ← [INPUT] │
|
||||||
|
│ │
|
||||||
|
│ Center: "Chrome DevTools MCP" │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Prompt Summary
|
||||||
|
|
||||||
|
Generate a chalkboard-style infographic showing Chrome DevTools MCP tool categories arranged in a circular flow pattern. The circle has 6 nodes representing the tool categories (Input Automation, Navigation, Emulation, Performance, Network, Debugging). Each node displays the tool count and key tool names in chalk-style lettering. The center shows "Chrome DevTools MCP" as the main concept. Arrows show clockwise flow indicating the continuous nature of browser automation workflow. Use authentic chalkboard aesthetic with black background (#1A1A1A), chalk white text, and colorful chalk accents (yellow, pink, blue, green) for visual hierarchy. Include chalk dust effects and hand-drawn imperfect lines throughout.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Hermes Custom Skills - Infographic Image Prompt
|
||||||
|
|
||||||
|
## Image Specifications
|
||||||
|
- **Type**: Infographic
|
||||||
|
- **Layout**: Circular Flow (cyclic process showing continuous recurring steps)
|
||||||
|
- **Style**: Chalkboard (black chalkboard background with colorful chalk drawing style)
|
||||||
|
- **Aspect Ratio**: 16:9
|
||||||
|
- **Language**: English
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
- Follow the circular-flow layout structure precisely for information architecture
|
||||||
|
- Apply chalkboard style aesthetics consistently throughout
|
||||||
|
- Create stylistically similar alternatives for any specific figures
|
||||||
|
- Keep information concise, highlight keywords and core concepts
|
||||||
|
- Use ample whitespace for visual clarity
|
||||||
|
- Maintain clear visual hierarchy
|
||||||
|
|
||||||
|
## Layout Guidelines
|
||||||
|
- Circular arrangement with 5 skill nodes evenly spaced around the circle
|
||||||
|
- Arrows showing clockwise direction flow
|
||||||
|
- No clear start/end (continuous cycle)
|
||||||
|
- Center holds main concept: "Hermes Automation Ecosystem"
|
||||||
|
- Title at top: "Hermes Custom Skills - Automation Workflows"
|
||||||
|
- Step labels at each node with skill names and taglines
|
||||||
|
- Brief descriptions near nodes
|
||||||
|
|
||||||
|
## Style Guidelines
|
||||||
|
- **Background**: Chalkboard Black (#1A1A1A) with realistic chalkboard texture, subtle scratches, dust particles, faint eraser marks
|
||||||
|
- **Typography**: Hand-drawn chalk lettering style with visible chalk texture, imperfect baseline, white or bright colored chalk for emphasis
|
||||||
|
- **Color Palette**:
|
||||||
|
- Background: #1A1A1A (Chalkboard Black)
|
||||||
|
- Primary Text: #F5F5F5 (Chalk White)
|
||||||
|
- Accent 1: #FFE566 (Chalk Yellow) - for skill names
|
||||||
|
- Accent 2: #FF9999 (Chalk Pink) - for highlights
|
||||||
|
- Accent 3: #66B3FF (Chalk Blue) - for connections
|
||||||
|
- Accent 4: #90EE90 (Chalk Green) - for success indicators
|
||||||
|
- **Visual Elements**: Hand-drawn chalk illustrations with sketchy imperfect lines, chalk dust effects, doodles (stars, arrows, circles), stick figures, connection lines with hand-drawn feel
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
**Title**: Hermes Custom Skills - Automation Workflows
|
||||||
|
|
||||||
|
**Center Concept**: Hermes Automation Ecosystem
|
||||||
|
|
||||||
|
**5 Skill Nodes (clockwise from top)**:
|
||||||
|
|
||||||
|
1. **blogwatcher-daily** - RSS Subscription Monitor - RSS monitoring + daily note generation - cronjob
|
||||||
|
|
||||||
|
2. **claude-code-executor** - Claude Code Task Delegation - Launch Claude Code via TMUX, send tasks, monitor completion - delegate_task
|
||||||
|
|
||||||
|
3. **claude-code-infographic-prompts** - Infographic Prompt Generator - Generate 3-part prompts for baoyu-infographic skill - manual
|
||||||
|
|
||||||
|
4. **marker-pdf-to-markdown** - PDF to Markdown Converter - High-precision PDF conversion with OCR, table, formula recognition - terminal
|
||||||
|
|
||||||
|
5. **whisper-audio-to-text** - Audio/Video Transcription - OpenAI Whisper for speech-to-text - terminal
|
||||||
|
|
||||||
|
**Bottom Note**: "5 Custom Skills | Powering Hermes Automation"
|
||||||
|
|
||||||
|
**Visual Elements**:
|
||||||
|
- Chalk dust particles around text
|
||||||
|
- Hand-drawn arrows connecting skills in clockwise cycle
|
||||||
|
- Small doodle icons (stars, checkmarks) near each skill
|
||||||
|
- Eraser smudge textures as subtle background variation
|
||||||
119
Hermes/xingzhi/hermes-skills-infographic-prompts-2026-04-20.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Hermes Custom Skills Infographic Prompts
|
||||||
|
|
||||||
|
**Date**: 2026-04-20
|
||||||
|
**Layout**: circular-flow
|
||||||
|
**Style**: chalkboard
|
||||||
|
**Aspect**: 16:9
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: System Prompt (Image Specifications + Core Principles)
|
||||||
|
|
||||||
|
**Image Specifications**
|
||||||
|
- **Type**: Infographic
|
||||||
|
- **Layout**: Circular Flow (cyclic process showing continuous recurring steps)
|
||||||
|
- **Style**: Chalkboard (black chalkboard background with colorful chalk drawing style)
|
||||||
|
- **Aspect Ratio**: 16:9
|
||||||
|
- **Language**: English
|
||||||
|
|
||||||
|
**Core Principles**
|
||||||
|
- Follow the circular-flow layout structure precisely for information architecture
|
||||||
|
- Apply chalkboard style aesthetics consistently throughout
|
||||||
|
- Create stylistically similar alternatives for any specific figures
|
||||||
|
- Keep information concise, highlight keywords and core concepts
|
||||||
|
- Use ample whitespace for visual clarity
|
||||||
|
- Maintain clear visual hierarchy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Style Locking Prompts (Layout Guidelines + Style Guidelines)
|
||||||
|
|
||||||
|
**Layout Guidelines (Circular Flow)**
|
||||||
|
- Circular arrangement with steps around the circle
|
||||||
|
- Arrows showing direction (clockwise flow)
|
||||||
|
- No clear start/end (continuous cycle)
|
||||||
|
- Center can hold main concept
|
||||||
|
- Steps around the circle: 5 skill nodes evenly spaced
|
||||||
|
- Icons per step representing each skill
|
||||||
|
- Title at top
|
||||||
|
- Step labels at each node
|
||||||
|
- Brief descriptions near nodes
|
||||||
|
- Center concept: "Hermes Automation Ecosystem"
|
||||||
|
|
||||||
|
**Style Guidelines (Chalkboard)**
|
||||||
|
- **Background**: Chalkboard Black (#1A1A1A) with realistic chalkboard texture, subtle scratches, dust particles, faint eraser marks
|
||||||
|
- **Typography**: Hand-drawn chalk lettering style with visible chalk texture, imperfect baseline, white or bright colored chalk for emphasis
|
||||||
|
- **Color Palette**:
|
||||||
|
- Background: #1A1A1A (Chalkboard Black)
|
||||||
|
- Primary Text: #F5F5F5 (Chalk White)
|
||||||
|
- Accent 1: #FFE566 (Chalk Yellow) - for skill names
|
||||||
|
- Accent 2: #FF9999 (Chalk Pink) - for highlights
|
||||||
|
- Accent 3: #66B3FF (Chalk Blue) - for connections
|
||||||
|
- Accent 4: #90EE90 (Chalk Green) - for success indicators
|
||||||
|
- **Visual Elements**: Hand-drawn chalk illustrations with sketchy imperfect lines, chalk dust effects, doodles (stars, arrows, circles), stick figures, connection lines with hand-drawn feel
|
||||||
|
- **Do**: Maintain authentic chalk texture, use imperfect hand-drawn quality, add chalk dust effects, create visual hierarchy with color variety
|
||||||
|
- **Don't**: Use perfect geometric shapes, create clean digital-looking lines, add photorealistic elements, use gradients
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Content Structure Prompts (Text Requirements + Structured Content)
|
||||||
|
|
||||||
|
**Text Requirements**
|
||||||
|
- All text must match chalkboard style treatment
|
||||||
|
- Main titles should be prominent and readable
|
||||||
|
- Key concepts should be visually emphasized
|
||||||
|
- Labels should be clear and appropriately sized
|
||||||
|
- Use English for all text content
|
||||||
|
|
||||||
|
**Structured Content**
|
||||||
|
|
||||||
|
**Title**: Hermes Custom Skills - Automation Workflows
|
||||||
|
|
||||||
|
**Center Concept**: Hermes Automation Ecosystem (in the middle of the circle)
|
||||||
|
|
||||||
|
**5 Skill Nodes (clockwise from top)**:
|
||||||
|
|
||||||
|
1. **blogwatcher-daily**
|
||||||
|
- Tagline: RSS Subscription Monitor
|
||||||
|
- Function: RSS monitoring + daily note generation
|
||||||
|
- Call method: cronjob
|
||||||
|
- Icon: RSS feed / newspaper symbol
|
||||||
|
|
||||||
|
2. **claude-code-executor**
|
||||||
|
- Tagline: Claude Code Task Delegation
|
||||||
|
- Function: Launch Claude Code via TMUX, send tasks, monitor completion
|
||||||
|
- Call method: delegate_task
|
||||||
|
- Icon: robot / terminal symbol
|
||||||
|
|
||||||
|
3. **claude-code-infographic-prompts**
|
||||||
|
- Tagline: Infographic Prompt Generator
|
||||||
|
- Function: Generate 3-part prompts for baoyu-infographic skill
|
||||||
|
- Call method: manual
|
||||||
|
- Icon: palette / image symbol
|
||||||
|
|
||||||
|
4. **marker-pdf-to-markdown**
|
||||||
|
- Tagline: PDF to Markdown Converter
|
||||||
|
- Function: High-precision PDF conversion with OCR, table, formula recognition
|
||||||
|
- Call method: terminal
|
||||||
|
- Icon: document conversion symbol
|
||||||
|
|
||||||
|
5. **whisper-audio-to-text**
|
||||||
|
- Tagline: Audio/Video Transcription
|
||||||
|
- Function: OpenAI Whisper for speech-to-text
|
||||||
|
- Call method: terminal
|
||||||
|
- Icon: microphone / audio wave symbol
|
||||||
|
|
||||||
|
**Connection Arrows**: Clockwise arrows showing continuous workflow between skills
|
||||||
|
|
||||||
|
**Bottom Note**: "5 Custom Skills | Powering Hermes Automation"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visual Elements to Include
|
||||||
|
|
||||||
|
- Wooden chalkboard frame border (optional)
|
||||||
|
- Chalk dust particles around text
|
||||||
|
- Hand-drawn arrows connecting skills in cycle
|
||||||
|
- Small doodle icons (stars, checkmarks) near each skill
|
||||||
|
- Mathematical/formula-style decorations (fitting the chalkboard theme)
|
||||||
|
- Eraser smudge textures as subtle background variation
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: "llm-wiki-sync Circular Flow"
|
||||||
|
topic: technical
|
||||||
|
data_type: cycle
|
||||||
|
complexity: moderate
|
||||||
|
point_count: 7
|
||||||
|
source_language: zh
|
||||||
|
user_language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Topic
|
||||||
|
A cyclical knowledge management pipeline that transforms raw notes into structured wiki pages through continuous LLM-powered ingestion, extraction, and reuse.
|
||||||
|
|
||||||
|
## Learning Objectives
|
||||||
|
After viewing this infographic, the viewer will understand:
|
||||||
|
1. The continuous circular flow of llm-wiki-sync from raw notes to reusable knowledge
|
||||||
|
2. The key extraction outputs: Summary, Claims, Entities, Concepts, Connections
|
||||||
|
3. How feedback and reuse complete the cycle back to new raw material
|
||||||
|
|
||||||
|
## Target Audience
|
||||||
|
- **Knowledge Level**: Intermediate technical audience
|
||||||
|
- **Context**: Developers and knowledge workers interested in AI-powered knowledge management
|
||||||
|
- **Expectations**: Clear understanding of the llm-wiki-sync pipeline and its cyclical nature
|
||||||
|
|
||||||
|
## Content Type Analysis
|
||||||
|
- **Data Structure**: Cyclic process with recurring steps
|
||||||
|
- **Key Relationships**: Raw → Ingest → Extract → Source Page → Graph/Site → Reuse → Raw (feedback loop)
|
||||||
|
- **Visual Opportunities**: Circular flow with nodes for each stage, arrows showing direction, center concept
|
||||||
|
|
||||||
|
## Key Data Points (Verbatim)
|
||||||
|
|
||||||
|
### Core Pipeline Steps
|
||||||
|
1. **Raw Note** - Original document in raw/ folder
|
||||||
|
2. **Ingest** - LLM analyzes and extracts structured information
|
||||||
|
3. **Extract** - Summary, Claims, Quotes, Entities, Concepts, Connections
|
||||||
|
4. **Source Page** - Structured wiki/sources/ page with frontmatter
|
||||||
|
5. **Graph & Site** - graph.json and Quartz static site generation
|
||||||
|
6. **Reuse** - Synthesize, query, create new content from structured knowledge
|
||||||
|
7. **Feedback Loop** - New raw notes created from reused knowledge
|
||||||
|
|
||||||
|
### Extraction Outputs
|
||||||
|
- **Summary**: 核心主题, 问题域, 方法/机制, 结论/价值
|
||||||
|
- **Key Claims**: Verifiable assertions extracted from text
|
||||||
|
- **Key Entities**: LaunchDarkly, HP, Christian Dior, etc.
|
||||||
|
- **Key Concepts**: RTO, RPO, Feature Flag, Kill Switch, Gradual Rollout
|
||||||
|
- **Connections**: depends_on, enables, provides relationships
|
||||||
|
|
||||||
|
### Key Quotes
|
||||||
|
- "RTO is about speed: how fast you get back online. RPO is about data: how much you can afford to lose."
|
||||||
|
- "Deploy != Release. Feature flags change this. You can deploy code to production without releasing it to users."
|
||||||
|
|
||||||
|
## Layout × Style Signals
|
||||||
|
- Content type: cycle → circular-flow
|
||||||
|
- Tone: technical educational → chalkboard
|
||||||
|
- Audience: developers → clear, legible, professional
|
||||||
|
- Complexity: moderate → balanced density with clear visual hierarchy
|
||||||
|
|
||||||
|
## Design Instructions (from user input)
|
||||||
|
- **Layout**: circular-flow (NOT linear - must emphasize recurring cycle)
|
||||||
|
- **Style**: chalkboard (dark background, hand-drawn chalk accents)
|
||||||
|
- **Aspect**: 16:9 landscape
|
||||||
|
- **Language**: English
|
||||||
|
- Circular flow showing: raw note -> ingest -> extract -> source page -> graph/static site -> reuse/feedback -> knowledge base
|
||||||
|
- Include core extraction outputs as recurring nodes/callouts
|
||||||
|
- Keep text concise and legible
|
||||||
|
- Dark chalkboard background with hand-drawn chalk accents
|
||||||
|
- Avoid clutter, make cycle visually clear and publication-ready
|
||||||
|
|
||||||
|
## Recommended Combinations
|
||||||
|
1. **circular-flow + chalkboard** (Recommended): Perfect match for cycle/process content with chalkboard aesthetic
|
||||||
|
2. **hub-spoke + technical-schematic**: For emphasizing central concepts with technical precision
|
||||||
|
3. **bento-grid + craft-handmade**: For multiple topic overview with friendly handmade feel
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
Create a professional infographic following these specifications:
|
||||||
|
|
||||||
|
## Image Specifications
|
||||||
|
|
||||||
|
- **Type**: Infographic
|
||||||
|
- **Layout**: circular-flow (Cyclic process showing continuous or recurring steps)
|
||||||
|
- **Style**: chalkboard (Black chalkboard background with colorful chalk drawing style)
|
||||||
|
- **Aspect Ratio**: 16:9
|
||||||
|
- **Language**: English
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
- Follow the layout structure precisely for information architecture
|
||||||
|
- Apply style aesthetics consistently throughout
|
||||||
|
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives
|
||||||
|
- Keep information concise, highlight keywords and core concepts
|
||||||
|
- Use ample whitespace for visual clarity
|
||||||
|
- Maintain clear visual hierarchy
|
||||||
|
|
||||||
|
## Text Requirements
|
||||||
|
|
||||||
|
- All text must match the specified style treatment
|
||||||
|
- Main titles should be prominent and readable
|
||||||
|
- Key concepts should be visually emphasized
|
||||||
|
- Labels should be clear and appropriately sized
|
||||||
|
- Use the specified language for all text content
|
||||||
|
|
||||||
|
## Layout Guidelines
|
||||||
|
|
||||||
|
- Circular arrangement
|
||||||
|
- Steps around the circle
|
||||||
|
- Arrows showing direction
|
||||||
|
- No clear start/end (continuous)
|
||||||
|
- Center can hold main concept
|
||||||
|
- Circle or ring shape
|
||||||
|
- Directional arrows
|
||||||
|
- Step nodes evenly spaced
|
||||||
|
- Icons per step
|
||||||
|
- Optional center element
|
||||||
|
|
||||||
|
## Style Guidelines
|
||||||
|
|
||||||
|
- **Background**: Chalkboard Black (#1A1A1A) or Dark Green-Black (#1C2B1C)
|
||||||
|
- **Texture**: Realistic chalkboard texture with subtle scratches, dust particles, and faint eraser marks
|
||||||
|
- **Typography**: Hand-drawn chalk lettering style with visible chalk texture. Imperfect baseline adds authenticity.
|
||||||
|
- **Color Palette**:
|
||||||
|
- Background: Chalkboard Black (#1A1A1A)
|
||||||
|
- Primary Text: Chalk White (#F5F5F5)
|
||||||
|
- Accent 1: Chalk Yellow (#FFE566)
|
||||||
|
- Accent 2: Chalk Pink (#FF9999)
|
||||||
|
- Accent 3: Chalk Blue (#66B3FF)
|
||||||
|
- Accent 4: Chalk Green (#90EE90)
|
||||||
|
- Accent 5: Chalk Orange (#FFB366)
|
||||||
|
- **Visual Elements**:
|
||||||
|
- Hand-drawn chalk illustrations with sketchy, imperfect lines
|
||||||
|
- Chalk dust effects around text and key elements
|
||||||
|
- Doodles: stars, arrows, underlines, circles, checkmarks
|
||||||
|
- Eraser smudges and chalk residue textures
|
||||||
|
- Wooden frame border optional
|
||||||
|
- Stick figures and simple icons
|
||||||
|
- Connection lines with hand-drawn feel
|
||||||
|
- **Style Rules**:
|
||||||
|
- Maintain authentic chalk texture on all elements
|
||||||
|
- Use imperfect, hand-drawn quality throughout
|
||||||
|
- Add subtle chalk dust and smudge effects
|
||||||
|
- Create visual hierarchy with color variety
|
||||||
|
- Include playful doodles and annotations
|
||||||
|
- DO NOT use perfect geometric shapes
|
||||||
|
- DO NOT create clean digital-looking lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Generate the infographic based on the content below:
|
||||||
|
|
||||||
|
# llm-wiki-sync: Turning Scattered Notes into a Reusable Knowledge Base
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A cyclical pipeline showing how raw notes are continuously transformed through LLM-powered ingestion into structured wiki pages, then feedback into the knowledge base for reuse.
|
||||||
|
|
||||||
|
## The Knowledge Pipeline Cycle (Center Concept)
|
||||||
|
The llm-wiki-sync pipeline operates as a continuous cycle, not a linear process.
|
||||||
|
7 stages in the cycle: Raw Note → Ingest → Extract → Source Page → Graph/Site → Reuse → Feedback Loop
|
||||||
|
|
||||||
|
## Circular Flow Diagram with 7 Stages:
|
||||||
|
|
||||||
|
1. **Raw Note** (Stage 1)
|
||||||
|
- Original documents stored in raw/ folder
|
||||||
|
- Contains unprocessed information awaiting structure
|
||||||
|
- Icon: Stack of paper/note icon
|
||||||
|
|
||||||
|
2. **Ingest** (Stage 2)
|
||||||
|
- LLM analyzes and extracts structured information
|
||||||
|
- Hermes skill triggers Claude Code for ingestion
|
||||||
|
- Context check against wiki/index.md prevents duplicates
|
||||||
|
- Icon: Brain/processing icon
|
||||||
|
|
||||||
|
3. **Extract** (Stage 3)
|
||||||
|
- Six key elements extracted from each document:
|
||||||
|
- Summary (核心主题, 问题域, 方法/机制, 结论/价值)
|
||||||
|
- Key Claims (Verifiable assertions)
|
||||||
|
- Key Quotes (Preserved citations)
|
||||||
|
- Key Entities (LaunchDarkly, HP, etc.)
|
||||||
|
- Key Concepts (RTO, RPO, Feature Flag, etc.)
|
||||||
|
- Connections (depends_on, enables, provides)
|
||||||
|
- Icon: Six circles/callouts
|
||||||
|
|
||||||
|
4. **Source Page** (Stage 4)
|
||||||
|
- Written to wiki/sources/<slug>.md
|
||||||
|
- Contains frontmatter and standard sections
|
||||||
|
- Links use [[PageName]] format
|
||||||
|
- Icon: Document/page icon
|
||||||
|
|
||||||
|
5. **Graph & Site** (Stage 5)
|
||||||
|
- graph.json: Machine-readable graph structure
|
||||||
|
- graph.html: Interactive visualization
|
||||||
|
- Quartz: Static site generation
|
||||||
|
- Icon: Network/graph icon
|
||||||
|
|
||||||
|
6. **Reuse** (Stage 6)
|
||||||
|
- Query, Synthesize, Write, Connect
|
||||||
|
- Icon: Multiple arrows pointing outward
|
||||||
|
|
||||||
|
7. **Feedback Loop** (Stage 7)
|
||||||
|
- New insights become new raw notes
|
||||||
|
- Cycle continues indefinitely
|
||||||
|
- Icon: Circular arrow completing the cycle
|
||||||
|
|
||||||
|
## Key Quotes (to include as callouts):
|
||||||
|
- "RTO is about speed: how fast you get back online. RPO is about data: how much you can afford to lose."
|
||||||
|
- "Deploy != Release. Feature flags change this. You can deploy code to production without releasing it to users."
|
||||||
|
|
||||||
|
## Design Requirements:
|
||||||
|
- Circular flow with 7 stages evenly spaced around a circle
|
||||||
|
- Clockwise arrow direction
|
||||||
|
- Center contains: "llm-wiki-sync" as main concept
|
||||||
|
- Each stage is a node with icon + label
|
||||||
|
- Extraction outputs (6 items) shown as callouts or inner ring
|
||||||
|
- Dark chalkboard background with hand-drawn chalk accents
|
||||||
|
- Chalk colors for visual hierarchy
|
||||||
|
- Imperfect, sketchy lines throughout
|
||||||
|
- Publication-ready quality
|
||||||
|
- NO clutter - only essential elements
|
||||||
|
- Clear hierarchy: title > headlines > labels > descriptions
|
||||||
|
|
||||||
|
## Text Labels (in English):
|
||||||
|
- Headline: "The Knowledge Pipeline Cycle"
|
||||||
|
- Subhead: "How llm-wiki-sync transforms scattered notes into a reusable knowledge base"
|
||||||
|
- Stage labels: "Raw Note", "Ingest", "Extract", "Source Page", "Graph & Site", "Reuse", "Feedback"
|
||||||
|
- Center: "llm-wiki-sync"
|
||||||
|
- Extraction labels: "Summary", "Claims", "Quotes", "Entities", "Concepts", "Connections"
|
||||||
|
|
||||||
|
## Key Constraints:
|
||||||
|
- 16:9 aspect ratio (landscape)
|
||||||
|
- All text in English
|
||||||
|
- Chalkboard style (dark background, chalk-like hand-drawn elements)
|
||||||
|
- Circular flow layout (NOT linear)
|
||||||
|
- Publication-ready, visually clear
|
||||||
|
- No clutter or excessive elements
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# llm-wiki-sync: Turning Scattered Notes into a Reusable Knowledge Base
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A cyclical pipeline showing how raw notes are continuously transformed through LLM-powered ingestion into structured wiki pages, then feedback into the knowledge base for reuse.
|
||||||
|
|
||||||
|
## Learning Objectives
|
||||||
|
The viewer will understand:
|
||||||
|
1. The continuous circular flow of llm-wiki-sync from raw notes to reusable knowledge
|
||||||
|
2. The six key extraction outputs: Summary, Claims, Quotes, Entities, Concepts, Connections
|
||||||
|
3. How feedback and reuse complete the cycle back to new raw material
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 1: The Circular Flow (Center Concept)
|
||||||
|
|
||||||
|
**Key Concept**: The llm-wiki-sync pipeline operates as a continuous cycle, not a linear process.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- 7 stages in the cycle: Raw Note → Ingest → Extract → Source Page → Graph/Site → Reuse → Feedback Loop
|
||||||
|
- Each stage feeds into the next, with feedback returning to the beginning
|
||||||
|
- The cycle is continuous and self-reinforcing
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: circular flow diagram
|
||||||
|
- Subject: 7 stages arranged in a circle with clockwise arrows
|
||||||
|
- Center label: "llm-wiki-sync Cycle"
|
||||||
|
- Treatment: chalk style with hand-drawn arrows connecting stages
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Headline: "The Knowledge Pipeline Cycle"
|
||||||
|
- Stage labels: "Raw Note", "Ingest", "Extract", "Source Page", "Graph/Site", "Reuse", "Feedback"
|
||||||
|
- Center: "llm-wiki-sync"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 2: Stage 1 — Raw Note (Input)
|
||||||
|
|
||||||
|
**Key Concept**: Raw notes are the starting point of the cycle.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- Original documents stored in raw/ folder
|
||||||
|
- Can be any format: markdown, text, research notes
|
||||||
|
- Contains unprocessed information awaiting structure
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: document/note icon
|
||||||
|
- Subject: Stack of paper or note icon
|
||||||
|
- Treatment: Chalk sketch style
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Label: "Raw Note"
|
||||||
|
- Description: "Original input"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 3: Stage 2 — Ingest (LLM Analysis)
|
||||||
|
|
||||||
|
**Key Concept**: The LLM analyzes raw notes and extracts structured information.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- Hermes skill triggers Claude Code for ingestion
|
||||||
|
- LLM reads and analyzes the full document
|
||||||
|
- Context check against wiki/index.md prevents duplicates
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: brain/processing icon
|
||||||
|
- Subject: Brain or gears with chalk lines
|
||||||
|
- Treatment: Hand-drawn chalk illustration
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Label: "Ingest"
|
||||||
|
- Description: "LLM Analysis"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 4: Stage 3 — Extract (Six Core Outputs)
|
||||||
|
|
||||||
|
**Key Concept**: Six key elements are extracted from each document.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
1. **Summary**: 核心主题, 问题域, 方法/机制, 结论/价值
|
||||||
|
2. **Key Claims**: Verifiable assertions extracted from text
|
||||||
|
3. **Key Quotes**: Preserved citations for reference
|
||||||
|
4. **Key Entities**: Named people, companies, products (e.g., LaunchDarkly, HP)
|
||||||
|
5. **Key Concepts**: Abstract terms that can be reused (e.g., RTO, RPO, Feature Flag)
|
||||||
|
6. **Connections**: Relationships between elements (depends_on, enables, provides)
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: 6 callout nodes around center
|
||||||
|
- Subject: Six boxes or bubbles representing extraction outputs
|
||||||
|
- Treatment: Chalk circles with icons inside each
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Headline: "Extraction Outputs"
|
||||||
|
- Labels: "Summary", "Claims", "Quotes", "Entities", "Concepts", "Connections"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 5: Stage 4 — Source Page (Structured Output)
|
||||||
|
|
||||||
|
**Key Concept**: Extracted information is written as a structured wiki source page.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- Written to wiki/sources/<slug>.md
|
||||||
|
- Contains frontmatter (id, title, type, tags, sources, last_updated)
|
||||||
|
- Standard sections: Summary, Key Claims, Key Quotes, Key Concepts, Key Entities, Connections, Contradictions
|
||||||
|
- Links use [[PageName]] format for interconnections
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: document/page icon
|
||||||
|
- Subject: Page with visible structure headers
|
||||||
|
- Treatment: Chalk sketch with text lines
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Label: "Source Page"
|
||||||
|
- Description: "wiki/sources/*.md"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 6: Stage 5 — Graph & Static Site
|
||||||
|
|
||||||
|
**Key Concept**: Structured pages generate knowledge graphs and static websites.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- graph.json: Machine-readable graph structure
|
||||||
|
- graph.html: Interactive visualization
|
||||||
|
- Quartz: Static site generation for sharing/export
|
||||||
|
- Connections become edges in the knowledge graph
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: network/graph icon
|
||||||
|
- Subject: Connected nodes representing knowledge graph
|
||||||
|
- Treatment: Chalk diagram with nodes and edges
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Label: "Graph & Site"
|
||||||
|
- Description: "graph.json + Quartz"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 7: Stage 6 — Reuse (Knowledge Application)
|
||||||
|
|
||||||
|
**Key Concept**: Structured knowledge enables multiple reuse scenarios.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- Query: Ask questions against the knowledge base
|
||||||
|
- Synthesize: Create new content from existing knowledge
|
||||||
|
- Write: Generate articles, reports from source material
|
||||||
|
- Connect: Link ideas across different source pages
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: multiple arrows pointing outward
|
||||||
|
- Subject: Reuse scenarios as icons (question, document, pen)
|
||||||
|
- Treatment: Chalk illustration
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Label: "Reuse"
|
||||||
|
- Sub-labels: "Query", "Synthesize", "Write", "Connect"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 8: Stage 7 — Feedback Loop (Continuous Cycle)
|
||||||
|
|
||||||
|
**Key Concept**: Reuse generates new raw notes, completing the cycle.
|
||||||
|
|
||||||
|
**Content**:
|
||||||
|
- New insights from synthesis become new raw notes
|
||||||
|
- Updated knowledge feeds back to raw/ folder
|
||||||
|
- Cycle continues indefinitely
|
||||||
|
- Each iteration strengthens the knowledge base
|
||||||
|
|
||||||
|
**Visual Element**:
|
||||||
|
- Type: circular arrow
|
||||||
|
- Subject: Feedback loop arrow returning to Raw Note stage
|
||||||
|
- Treatment: Large chalk arrow completing the circle
|
||||||
|
|
||||||
|
**Text Labels**:
|
||||||
|
- Label: "Feedback Loop"
|
||||||
|
- Description: "New notes → Cycle repeats"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Points (Verbatim)
|
||||||
|
|
||||||
|
### Key Quotes
|
||||||
|
- "RTO is about speed: how fast you get back online. RPO is about data: how much you can afford to lose."
|
||||||
|
- "Deploy != Release. Feature flags change this. You can deploy code to production without releasing it to users."
|
||||||
|
|
||||||
|
### Key Entities
|
||||||
|
- LaunchDarkly (Feature Flag management platform)
|
||||||
|
- HP (example enterprise)
|
||||||
|
- Christian Dior (example case)
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
- RTO (Recovery Time Objective)
|
||||||
|
- RPO (Recovery Point Objective)
|
||||||
|
- Feature Flag (特性开关)
|
||||||
|
- Kill Switch (紧急关闭机制)
|
||||||
|
- 渐进式发布 (Gradual Rollout)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Instructions
|
||||||
|
|
||||||
|
### Layout Preferences
|
||||||
|
- Circular flow with 7 stages evenly spaced around a circle
|
||||||
|
- Clockwise arrow direction
|
||||||
|
- Center contains the main concept "llm-wiki-sync"
|
||||||
|
- Each stage is a node with icon + label
|
||||||
|
- Extraction outputs (6 items) shown as callouts or inner ring
|
||||||
|
|
||||||
|
### Style Preferences
|
||||||
|
- Chalkboard: Dark background (#1A1A1A)
|
||||||
|
- Hand-drawn chalk style for all elements
|
||||||
|
- Chalk colors: white, yellow, pink, blue, green, orange
|
||||||
|
- Imperfect, sketchy lines throughout
|
||||||
|
- Chalk dust effects for authenticity
|
||||||
|
|
||||||
|
### Text Requirements
|
||||||
|
- All text in English
|
||||||
|
- Legible font sizes (minimum 14pt for labels)
|
||||||
|
- Clear hierarchy: title > headlines > labels > descriptions
|
||||||
|
- Ample whitespace between stages
|
||||||
|
|
||||||
|
### Visual Clarity
|
||||||
|
- Avoid clutter - only essential elements
|
||||||
|
- Each stage should be clearly distinguishable
|
||||||
|
- Arrows should clearly indicate flow direction
|
||||||
|
- Publication-ready quality
|
||||||
25
Hermes/xingzhi/musicbrainz_api_test.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
# 获取曲目信息的函数
|
||||||
|
|
||||||
|
def get_recording_info(mbid):
|
||||||
|
url = f'https://musicbrainz.org/ws/2/recording/{mbid}?fmt=json'
|
||||||
|
response = requests.get(url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json() # 返回曲目信息
|
||||||
|
else:
|
||||||
|
return {'error': 'Not Found', 'status_code': response.status_code}
|
||||||
|
|
||||||
|
# 测试 MBID 列表
|
||||||
|
mbids = [
|
||||||
|
'a34ecc5d-388e-40fb-a2a2-5354db8fdfaa', # 示例 MBID
|
||||||
|
'c6f24108-1f3f-4bf7-a52d-818ec956c2de',
|
||||||
|
'cd2d5cc0-7cfa-4f7c-99f5-4fd05b07873c'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 循环查询每个 MBID
|
||||||
|
for mbid in mbids:
|
||||||
|
info = get_recording_info(mbid)
|
||||||
|
print(f'情報for MBID {mbid}:')
|
||||||
|
print(info)
|
||||||
|
print('-' * 40) # 分隔线
|
||||||
51
Hermes/xingzhi/养龙虾封面提示词-2026-04-20.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: 养龙虾5天血泪史:我的AI Agent为什么总失忆?OpenClaw 记忆调试全记录
|
||||||
|
type: metaphor
|
||||||
|
palette: warm
|
||||||
|
rendering: digital
|
||||||
|
text: title-subtitle
|
||||||
|
mood: balanced
|
||||||
|
---
|
||||||
|
|
||||||
|
请为这篇文章生成封面图。
|
||||||
|
|
||||||
|
## 视觉概念
|
||||||
|
|
||||||
|
以"金鱼记忆"为核心隐喻:一条卡通金鱼游在透明的水缸中,金鱼的大脑位置是一个空白的问号框架,周围漂浮着记忆碎片(文字泡泡、对话气泡、代码片段),碎片正在逐渐消散到缸外。底部有简单的齿轮和调试工具元素,暗示"调试"过程。
|
||||||
|
|
||||||
|
## 主视觉元素
|
||||||
|
|
||||||
|
- 金鱼:卡通风格,圆形身体,大眼睛,尾巴呈波浪形游动姿态
|
||||||
|
- 空白大脑:空心问号形状,位于金鱼头部位置
|
||||||
|
- 记忆碎片:5-6个椭圆形气泡,包含模糊的文字轮廓、对话符号、代码片段
|
||||||
|
- 水缸:简单几何圆形边框,内部有微妙的涟漪效果
|
||||||
|
- 调试工具:底部角落有小型齿轮、螺丝刀、代码括号图标
|
||||||
|
|
||||||
|
## 配色方案(warm palette)
|
||||||
|
|
||||||
|
- 主色:温暖橙色 #F5A623(用于金鱼身体)
|
||||||
|
- 辅色:柔和珊瑚色 #FF8C74(用于记忆碎片)
|
||||||
|
- 强调色:深琥珀色 #C47A2B(用于鱼鳍和调试工具)
|
||||||
|
- 背景:米白色 #FFF8F0 到浅杏色 #FFE8D6 渐变
|
||||||
|
- 文字色:深棕色 #4A3728
|
||||||
|
|
||||||
|
## 渲染风格(digital)
|
||||||
|
|
||||||
|
- 干净的矢量线条,精确边缘
|
||||||
|
- 平滑渐变,无粗糙纹理
|
||||||
|
- 轻微阴影创造层次感
|
||||||
|
- 像专业UI插图一样现代简洁
|
||||||
|
|
||||||
|
## 文字布局
|
||||||
|
|
||||||
|
- 标题:"养龙虾5天血泪史" 使用大号粗体字(金鱼上方右侧)
|
||||||
|
- 副标题:"我的AI Agent为什么总失忆?" 使用较小字号(标题下方)
|
||||||
|
- 字体:现代无衬线体,清晰易读
|
||||||
|
- 文字颜色:深棕色 #4A3728
|
||||||
|
|
||||||
|
## 氛围(balanced)
|
||||||
|
|
||||||
|
- 中等对比度
|
||||||
|
- 正常饱和度
|
||||||
|
- 视觉重量平衡
|
||||||
|
- 温暖友好但不强烈
|
||||||
22
Hermes/xingzhi/常用任务指令.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
## 信息图任务指令
|
||||||
|
|
||||||
|
请用**claude-code-infographic-build** 这个技能为`Hermes/xingzhi/Hermes自定义技能说明` 这篇笔记生成信息图。
|
||||||
|
- 语言:英文
|
||||||
|
- 布局:circular-flow
|
||||||
|
- 风格:chalkboard
|
||||||
|
- 比例:16:9
|
||||||
|
|
||||||
|
|
||||||
|
## 封面图任务指令
|
||||||
|
请用**claude-code-executor**技能启动Claude Code
|
||||||
|
使用 **baoyu-cover-image** 技能为以下内容生成封面图提示词:
|
||||||
|
|
||||||
|
1. 读取 Hermes/xingzhi/wiki-sync-setup-2026-04-16.md 这篇笔记
|
||||||
|
2. 用**baoyu-cover-image** 生成一张封面图
|
||||||
|
- **类型 (Type)**:`hero`、`conceptual`、`typography`、`metaphor`、`scene`、`minimal`
|
||||||
|
- **配色 (Palette)**:`warm`、`elegant`、`cool`、`dark`、`earth`、`vivid`、`pastel`、`mono`、`retro`
|
||||||
|
- **渲染 (Rendering)**:`flat-vector`、`hand-drawn`、`painterly`、`digital`、`pixel`、`chalk`
|
||||||
|
- **文字 (Text)**:`none`、`title-only`(默认)、`title-subtitle`、`text-rich`
|
||||||
|
- **氛围 (Mood)**:`subtle`、`balanced`(默认)、`bold`
|
||||||
|
3. 不要生成图片,只输出提示词
|
||||||
|
4. 输出提示词到 Hermes/xingzhi/ 新建一个笔记
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
## 信息图任务指令
|
|
||||||
|
|
||||||
请用**claude-code-executor**技能启动Claude Code
|
|
||||||
使用 **baoyu-infographic** 技能为以下内容生成信息图提示词:
|
|
||||||
|
|
||||||
1. 读取 Hermes/xingzhi/wiki-sync-setup-2026-04-16.md 这篇笔记
|
|
||||||
2. 用baoyu-infographic 生成一张信息图
|
|
||||||
- 语言:英文
|
|
||||||
- 布局:circular-flow
|
|
||||||
- 风格:chalkboard
|
|
||||||
- 比例:16:9
|
|
||||||
- 不要生成图片,只输出提示词
|
|
||||||
3. 为了后续图片能保持一致的风格,提示词生成必须输出三部分提示词。
|
|
||||||
- 系统提示词(Image Specifications + Core Principles)
|
|
||||||
- 风格锁定提示词(Layout Guidelines + Style Guidelines)
|
|
||||||
- 内容结构提示词 (Text Requirements)
|
|
||||||
4. 输出提示词到 Hermes/xingzhi/ 新建一个笔记
|
|
||||||
|
|
||||||
## 封面图任务指令
|
|
||||||
请用**claude-code-executor**技能启动Claude Code
|
|
||||||
使用 **baoyu-cover-image** 技能为以下内容生成封面图提示词:
|
|
||||||
|
|
||||||
1. 读取 Hermes/xingzhi/wiki-sync-setup-2026-04-16.md 这篇笔记
|
|
||||||
2. 用**baoyu-cover-image** 生成一张封面图
|
|
||||||
- **类型 (Type)**:`hero`、`conceptual`、`typography`、`metaphor`、`scene`、`minimal`
|
|
||||||
- **配色 (Palette)**:`warm`、`elegant`、`cool`、`dark`、`earth`、`vivid`、`pastel`、`mono`、`retro`
|
|
||||||
- **渲染 (Rendering)**:`flat-vector`、`hand-drawn`、`painterly`、`digital`、`pixel`、`chalk`
|
|
||||||
- **文字 (Text)**:`none`、`title-only`(默认)、`title-subtitle`、`text-rich`
|
|
||||||
- **氛围 (Mood)**:`subtle`、`balanced`(默认)、`bold`
|
|
||||||
3. 不要生成图片,只输出提示词
|
|
||||||
4. 输出提示词到 Hermes/xingzhi/ 新建一个笔记
|
|
||||||
127
Hermes/xingzhi/用 LLM把零散资料变成可复用的知识库 —— llm-wiki-sync 的实现与示例解析.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
|
||||||
|
**副标题**:如何把每一份笔记,通过 llm-wiki-sync 自动分析与提炼成结构化的页面、实体与概念,以便长期检索与复用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前言与来源说明
|
||||||
|
|
||||||
|
本方案借鉴并整合了几条重要线索:
|
||||||
|
- Andrej Karpathy 对“LLM Wiki”理念的阐述(将知识以可被大模型直接消费的结构化方式保存):https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f
|
||||||
|
- 我们的实现基于开源项目 SamurAI 的 llm-wiki-agent(https://github.com/SamurAIGPT/llm-wiki-agent),在其基础上扩展了企业化的 ingest 流程、Cron 调度与 Hermes skill(llm-wiki-sync)。
|
||||||
|
- 最终静态化展示使用 Quartz(https://github.com/jackyzha0/quartz),把生成的 wiki 内容导出为可浏览、可分享的静态站点。
|
||||||
|
|
||||||
|
下面重点介绍 llm-wiki-sync 如何把一篇笔记做结构化分析与提炼,并用仓库中的实例(wiki/sources/RTO-vs-RPO-Key-Differences-for-Modern-Disaster-Recovery)作为逐项说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## llm-wiki-sync 的目标与核心能力
|
||||||
|
|
||||||
|
核心目标:把原始文档(raw/)自动转成结构化的 wiki source 页面,抽取关键要素(Summary、Key Claims、Key Quotes、Key Concepts、Key Entities、Connections),并记录 ingest 日志、差异与审计信息,供后续检索、合成与内容再生产使用。
|
||||||
|
|
||||||
|
关键能力:
|
||||||
|
- 文本解析与语义压缩:把长文本压缩为 2–4 句高密度 summary。
|
||||||
|
- 声明抽取(claim extraction):识别文中明确的结论与可验证断言。
|
||||||
|
- 实体与概念抽取(NER + linking):识别人名/公司/工具/概念,并把它们标准化为 wiki 实体页([[Name]])。
|
||||||
|
- 关系发现(connections):把句子级别的语义关系转成图边(A → depends_on → B)。
|
||||||
|
- 模板化输出:固定页面头(frontmatter)+ 标准段落(Summary / Key Claims / Quotes / Concepts / Entities / Connections / Contradictions)。
|
||||||
|
- 审计与可回滚:每次 ingest 都写入 wiki/log.md,并可通过 git/checkpoint 回滚变更。
|
||||||
|
|
||||||
|
实现技术栈要点:Hermes(skill 调用)、Claude Code / agent(可选)、llm-wiki-agent 基础脚本、以及最终的静态化工具 Quartz。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 从笔记到 Source Page
|
||||||
|
|
||||||
|
仓库中的源页面:wiki/sources/RTO-vs-RPO-Key-Differences-for-Modern-Disaster-Recovery.md
|
||||||
|
|
||||||
|
下面逐项展示 llm-wiki-sync 针对该文档所做的提取结果(摘自生成的 source 页面):
|
||||||
|
|
||||||
|
1) Summary(Summary)
|
||||||
|
- 核心主题:RTO(恢复时间目标)与 RPO(恢复点目标)的定义、区别及在现代持续交付中的应用
|
||||||
|
- 问题域:灾难恢复规划、发布风险管控
|
||||||
|
- 方法/机制:通过 Feature Flag 实现秒级 RTO 和低 RPO
|
||||||
|
- 结论/价值:预防优于恢复,Feature Flag 将部署事故从灾难转为非事件
|
||||||
|
|
||||||
|
说明:Summary 由模型将整篇文章的主旨、问题背景、关键方法与结论压缩为 2–4 条,便于快速检索与索引。
|
||||||
|
|
||||||
|
2) Key Claims(断言提取)
|
||||||
|
- RTO 衡量系统恢复速度:允许的最大停机时间
|
||||||
|
- RPO 衡量数据保护:可接受的最大数据丢失量
|
||||||
|
- 传统灾备聚焦硬件故障,现代风险更多来自代码变更(部署 bug、数据库迁移、AI 模型更新等)
|
||||||
|
- Feature Flag 将 RTO 从小时级降至秒级,同时保护 RPO
|
||||||
|
- 应用分层策略(Critical / Important / Nice-to-have)对应不同的 RTO/RPO 指标
|
||||||
|
|
||||||
|
说明:断言提取用于建立事实层(fact layer),后续可自动化做一致性检查与冲突检测(Contradictions 段)。
|
||||||
|
|
||||||
|
3) Key Quotes(关键引用)
|
||||||
|
- “RTO is about speed: how fast you get back online. RPO is about data: how much you can afford to lose.”
|
||||||
|
- “Deploy != Release. Feature flags change this. You can deploy code to production without releasing it to users.”
|
||||||
|
|
||||||
|
说明:保留可引用原句,便于在后续合成(如写作、演讲稿)中引用来源。
|
||||||
|
|
||||||
|
4) Key Concepts(概念抽取)
|
||||||
|
- RTO(Recovery Time Objective)
|
||||||
|
- RPO(Recovery Point Objective)
|
||||||
|
- Feature Flag(特性开关)
|
||||||
|
- Kill Switch(紧急关闭机制)
|
||||||
|
- 渐进式发布(Gradual Rollout)
|
||||||
|
|
||||||
|
说明:概念会被标准化为 wiki 的 concept 页面(wiki/concepts/),用于聚合所有提到该概念的 source 页面。
|
||||||
|
|
||||||
|
5) Key Entities(实体抽取)
|
||||||
|
- LaunchDarkly(Feature Flag 管理平台)
|
||||||
|
- HP(示例企业)
|
||||||
|
- Christian Dior(示例企业 — 文档中作为案例提及)
|
||||||
|
|
||||||
|
说明:实体会被规范化为 wiki/entities/ 下的页面,并且 source 页面会在 sources 列表保留原始链接与引用。
|
||||||
|
|
||||||
|
6) Connections(关系构建)
|
||||||
|
- RTO ← depends_on ← Feature Flag
|
||||||
|
- RPO ← depends_on ← Feature Flag
|
||||||
|
- LaunchDarkly → provides → Feature Flag
|
||||||
|
- Feature Flag ← enables ← 渐进式发布
|
||||||
|
|
||||||
|
说明:Connections 用于图谱构建(graph/graph.json),后续能在可视化页面(graph.html)展示节点与边。
|
||||||
|
|
||||||
|
7) Contradictions(冲突检测)
|
||||||
|
- 当前文档无明显与现有 wiki 冲突的声明;若检测到冲突,llm-wiki-sync 会把冲突条目列出并标注来源,供人工审查。
|
||||||
|
![[IMG-20260420160439552.png|872]]
|
||||||
|
![[IMG-20260420160439600.png]]
|
||||||
|
---
|
||||||
|
|
||||||
|
## llm-wiki-sync 的典型运行步骤(工程视角)
|
||||||
|
|
||||||
|
1. 读取 raw/<path>,解析 frontmatter/元数据(若缺失则询回填)。
|
||||||
|
2. 检索 wiki/index.md 与 overview.md,获取上下文(避免重复 ingest)。
|
||||||
|
3. 用 LLM 生成 Source Page(Summary / Key Claims / Quotes / Concepts / Entities / Connections / Contradictions)。
|
||||||
|
4. 将结果写入 wiki/sources/<slug>.md,并更新 wiki/index.md、wiki/overview.md;如有新实体/概念也生成对应页面草稿。
|
||||||
|
5. 记录日志:在 wiki/log.md 追加 ingest 记录(时间、文件、摘要、状态)。
|
||||||
|
6. 可选:触发 graph 重建(增量或全量),并把 graph/graph.json、graph.html 更新到站点。
|
||||||
|
7. 通知:完成后通过 Hermes 的通知机制(deliver=origin 或 Telegram 指定 chat)告知负责人。
|
||||||
|
|
||||||
|
并发与配额注意:单文件 ingest 优先,批量操作分批(每批 2~3 篇);对接外部 agent/Claude Code 时避免并发超配额。
|
||||||
|
|
||||||
|
审计与回滚:每次 ingest 前执行 git branch 或 checkpoint;如需回滚,可用 git revert 或恢复 checkpoint。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结与扩展
|
||||||
|
|
||||||
|
- llm-wiki-sync 把 Karpathy 关于 LLM Wiki 的理念落地为可执行的工程流程:把知识以结构化表征保存,使得大模型既是“读者”也是“执行者”。
|
||||||
|
- 在 Obsidian 中可以直接通过关系图(graph view)查看笔记间的关联;在 llm-wiki-agent 中可以通过 wiki-graph 构建并在 graph.html / graph/graph.json 中可视化展示。
|
||||||
|
- 我们的实现基于 SamurAI 的 llm-wiki-agent,并在其上加入了企业级的同步、审计与 Hermes skill 封装,最终通过 Quartz 静态站把生成的 wiki 内容对外展示与分享。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infographic Asset
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Infographic**: The Knowledge Pipeline Cycle — circular flow showing how llm-wiki-sync transforms scattered notes into a reusable knowledge base.
|
||||||
|
- Layout: circular-flow (7-stage cycle)
|
||||||
|
- Style: chalkboard (dark background, hand-drawn chalk accents)
|
||||||
|
- Aspect ratio: 16:9
|
||||||
|
- Prompt file: `infographic/llm-wiki-sync-circular-flow/prompts/infographic.md`
|
||||||
|
- Image: `llm-wiki-sync-circular-flow-infographic.png`
|
||||||
|
|
||||||
65
Hermes/xingzhi/自动分析和完善MP3信息.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 自动分析和完善 MP3 信息的指南
|
||||||
|
|
||||||
|
## 1. 提取音频特征和元数据
|
||||||
|
使用音频指纹技术(如 **AcoustID**)来获取 MP3 文件的特征和元数据。
|
||||||
|
- **安装 Chromaprint**: 通过以下命令安装 Chromaprint。
|
||||||
|
```bash
|
||||||
|
sudo apt-get install chromaprint
|
||||||
|
```
|
||||||
|
|
||||||
|
- **生成音频指纹**: 使用 `fpcalc` 命令生成 MP3 文件的指纹。
|
||||||
|
```bash
|
||||||
|
fpcalc yourfile.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 调用 AcoustID API
|
||||||
|
使用生成的指纹调用 AcoustID API,获取音频文件的音乐信息。示例请求:
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://api.acoustid.org/v2/lookup?client=YOUR_ACOUSTID_API_KEY&format=json&duration=DURATION&fingerprint=YOUR_FINGERPRINT"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 从 AcoustID 获取 MusicBrainz ID
|
||||||
|
- AcoustID 的响应将包含与该指纹匹配的 MusicBrainz ID(MBID)、艺术家、专辑等信息。
|
||||||
|
|
||||||
|
## 4. 使用 MusicBrainz API 获取详细信息
|
||||||
|
使用获取的 MBID 查询详细的音乐信息:
|
||||||
|
```bash
|
||||||
|
GET https://musicbrainz.org/ws/2/artist/ARTIST_MBID?fmt=json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 更新 MP3 的元数据
|
||||||
|
使用 Python 的 `mutagen` 库更新 MP3 文件的元数据。示例代码如下:
|
||||||
|
```python
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from mutagen.id3 import ID3, TIT2, TPE1, TALB
|
||||||
|
|
||||||
|
def update_metadata(file_path, title, artist, album):
|
||||||
|
audio = MP3(file_path, ID3=ID3)
|
||||||
|
audio['TIT2'] = TIT2(encoding=3, text=title) # 标题
|
||||||
|
audio['TPE1'] = TPE1(encoding=3, text=artist) # 艺术家
|
||||||
|
audio['TALB'] = TALB(encoding=3, text=album) # 专辑
|
||||||
|
audio.save()
|
||||||
|
|
||||||
|
# 调用示例
|
||||||
|
update_metadata('yourfile.mp3', 'Correct Title', 'Correct Artist', 'Correct Album')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 添加专辑图片信息
|
||||||
|
可以使用 `mutagen` 库来在 MP3 文件中添加专辑图片:
|
||||||
|
```python
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from mutagen.id3 import ID3, APIC
|
||||||
|
|
||||||
|
def add_album_art(mp3_file, image_file):
|
||||||
|
audio = MP3(mp3_file, ID3=ID3)
|
||||||
|
with open(image_file, 'rb') as img_file:
|
||||||
|
img_data = img_file.read()
|
||||||
|
audio['APIC'] = APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=img_data)
|
||||||
|
audio.save()
|
||||||
|
|
||||||
|
# 调用示例
|
||||||
|
add_album_art('yourfile.mp3', 'cover_image.jpg')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 结论
|
||||||
|
此方法提供了一种系统化的方式来自动分析 MP3 文件并完善文件的信息,通过音乐指纹识别、使用 API 获取数据以及更新元数据的结合,实现音乐库的维护和信息准确性。
|
||||||
193
Hermes/yunzhi/CUE+WAV分割与高质量MP3转换.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
title: CUE + WAV 分割与高质量 MP3 转换
|
||||||
|
date: 2026-05-20
|
||||||
|
tags: [audio, cue, wav, mp3, ffmpeg, linux]
|
||||||
|
---
|
||||||
|
|
||||||
|
# CUE + WAV 分割与高质量 MP3 转换
|
||||||
|
|
||||||
|
这篇笔记记录如何在命令行下,把一张专辑常见的 `cue + wav` 形式,切分成单曲,并转换成高质量 MP3。
|
||||||
|
|
||||||
|
## 一、基本概念
|
||||||
|
|
||||||
|
- `cue`:曲目索引与标签信息,记录每首歌的开始时间、标题、歌手等。
|
||||||
|
- `wav`:整张专辑的无损音频文件。
|
||||||
|
- 典型场景:一张专辑只有一个大 WAV 文件,加一个 CUE 文件。
|
||||||
|
|
||||||
|
目标:
|
||||||
|
1. 按 CUE 的时间点切分成单轨。
|
||||||
|
2. 转成高质量 MP3。
|
||||||
|
3. 尽量保留标题、歌手、专辑等标签。
|
||||||
|
|
||||||
|
## 二、推荐工具
|
||||||
|
|
||||||
|
在 Ubuntu 上建议安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install cuetools shntool lame ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- `cuetools`:提供 `cuebreakpoints`、`cuetag`
|
||||||
|
- `shntool`:负责按断点切分音频
|
||||||
|
- `ffmpeg`:负责编码成 MP3
|
||||||
|
- `lame`:MP3 编码器,ffmpeg 也会调用到它的能力
|
||||||
|
|
||||||
|
## 三、按 CUE 切分 WAV
|
||||||
|
|
||||||
|
### 方式 1:先切分,再转码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuebreakpoints "album.cue" | shnsplit -o wav "album.wav"
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
- 生成 `01.wav`、`02.wav`、`03.wav` ……
|
||||||
|
- 每个文件对应 CUE 里的一个曲目
|
||||||
|
|
||||||
|
### 注意
|
||||||
|
|
||||||
|
- `cue` 文件中引用的 wav 文件名,必须和实际文件名一致。
|
||||||
|
- 如果文件名不一致,先修正 CUE 里的 `FILE` 行,或把 wav 文件重命名。
|
||||||
|
- 中文文件名一般没问题,但终端编码和 shell 引号要保持正确。
|
||||||
|
|
||||||
|
## 四、转换为高质量 MP3
|
||||||
|
|
||||||
|
### 方案 A:VBR 高质量,推荐
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for f in *.wav; do
|
||||||
|
ffmpeg -y -i "$f" -codec:a libmp3lame -q:a 0 "${f%.wav}.mp3"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- `-q:a 0` 表示最高质量的 VBR 档位之一
|
||||||
|
- 一般适合日常听歌和保留尽可能好的音质
|
||||||
|
|
||||||
|
### 方案 B:固定 320k
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for f in *.wav; do
|
||||||
|
ffmpeg -y -i "$f" -codec:a libmp3lame -b:a 320k "${f%.wav}.mp3"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 码率固定,体积更可预测
|
||||||
|
- 如果你偏好统一规格,可以选这个
|
||||||
|
|
||||||
|
## 五、把 CUE 标签写回 MP3
|
||||||
|
|
||||||
|
切分并转码后,可以把 CUE 里的标签批量写进 MP3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuetag "album.cue" *.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
通常会把以下信息写入:
|
||||||
|
- 曲名
|
||||||
|
- 专辑名
|
||||||
|
- 歌手
|
||||||
|
- 轨道号
|
||||||
|
|
||||||
|
## 六、一键脚本
|
||||||
|
|
||||||
|
下面是一个可直接使用的一键脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CUE="$1"
|
||||||
|
WAV="$2"
|
||||||
|
OUTDIR="${3:-output}"
|
||||||
|
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
cd "$OUTDIR"
|
||||||
|
|
||||||
|
cuebreakpoints "$CUE" | shnsplit -o wav "$WAV"
|
||||||
|
|
||||||
|
for f in *.wav; do
|
||||||
|
ffmpeg -y -i "$f" -codec:a libmp3lame -q:a 0 "${f%.wav}.mp3"
|
||||||
|
done
|
||||||
|
|
||||||
|
cuetag "$CUE" *.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
用法:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x split_cue_to_mp3.sh
|
||||||
|
./split_cue_to_mp3.sh "album.cue" "album.wav"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、实战建议
|
||||||
|
|
||||||
|
### 1. 先保留无损源文件
|
||||||
|
|
||||||
|
MP3 即使是高质量编码,依然是有损格式。建议保留:
|
||||||
|
- 原始 WAV
|
||||||
|
- 原始 CUE
|
||||||
|
- 转换后的 MP3
|
||||||
|
|
||||||
|
### 2. 输出目录独立管理
|
||||||
|
|
||||||
|
不要直接在源目录操作,建议每张专辑一个输出目录,便于回溯。
|
||||||
|
|
||||||
|
### 3. 中文文件名注意引号
|
||||||
|
|
||||||
|
所有路径都用双引号包裹:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"我的专辑.cue"
|
||||||
|
"我的专辑.wav"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 出现标签错位时先检查 CUE
|
||||||
|
|
||||||
|
如果曲名或分轨偏移不对,先检查:
|
||||||
|
- `INDEX 01` 时间点是否正确
|
||||||
|
- `FILE` 行是否指向正确的 wav 文件
|
||||||
|
- wav 是否已经被裁剪过
|
||||||
|
|
||||||
|
## 八、常见问题
|
||||||
|
|
||||||
|
### 1. cuebreakpoints 报错找不到文件
|
||||||
|
|
||||||
|
说明 CUE 中的文件名和实际文件名不一致。
|
||||||
|
|
||||||
|
### 2. shnsplit 生成的文件顺序不对
|
||||||
|
|
||||||
|
先检查 CUE 时间轴是否写错,或者原始 WAV 是否有静默开头/结尾。
|
||||||
|
|
||||||
|
### 3. ffmpeg 转码失败
|
||||||
|
|
||||||
|
检查是否安装了 `ffmpeg` 和 `libmp3lame` 支持。
|
||||||
|
|
||||||
|
### 4. cuetag 没有写入标签
|
||||||
|
|
||||||
|
确认:
|
||||||
|
- CUE 文件语法正确
|
||||||
|
- MP3 文件名和轨道对应关系未被破坏
|
||||||
|
|
||||||
|
## 九、推荐命令组合
|
||||||
|
|
||||||
|
如果只想快速执行,最常用的组合是:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cuebreakpoints "album.cue" | shnsplit -o wav "album.wav"
|
||||||
|
for f in *.wav; do ffmpeg -y -i "$f" -codec:a libmp3lame -q:a 0 "${f%.wav}.mp3"; done
|
||||||
|
cuetag "album.cue" *.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十、结论
|
||||||
|
|
||||||
|
如果目标是“命令行下稳定地把 cue+wav 专辑切成高质量 mp3”,推荐流程是:
|
||||||
|
|
||||||
|
1. `cuebreakpoints + shnsplit` 切轨
|
||||||
|
2. `ffmpeg + libmp3lame -q:a 0` 转码
|
||||||
|
3. `cuetag` 回写标签
|
||||||
|
|
||||||
|
这套组合简单、稳定、可批处理,适合长期使用。
|
||||||
222
Hermes/yunzhi/技术笔记/django-tenants 完整配置指南.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
---
|
||||||
|
title: django-tenants 完整配置指南
|
||||||
|
created: 2026-04-21
|
||||||
|
tags: [django, django-tenants, postgresql, saas, multi-tenant]
|
||||||
|
category: 技术笔记
|
||||||
|
---
|
||||||
|
|
||||||
|
# django-tenants 完整配置指南
|
||||||
|
|
||||||
|
## 一、安装依赖
|
||||||
|
|
||||||
|
pip install django-tenants psycopg2-binary django-jazzmin
|
||||||
|
|
||||||
|
## 二、项目目录结构
|
||||||
|
|
||||||
|
myproject/
|
||||||
|
├── config/
|
||||||
|
│ ├── settings/
|
||||||
|
│ │ ├── base.py
|
||||||
|
│ │ ├── development.py
|
||||||
|
│ │ └── production.py
|
||||||
|
│ ├── urls.py
|
||||||
|
│ └── wsgi.py
|
||||||
|
├── apps/
|
||||||
|
│ ├── tenants/
|
||||||
|
│ ├── subscription/
|
||||||
|
│ ├── accounts/
|
||||||
|
│ ├── listings/
|
||||||
|
│ ├── clients/
|
||||||
|
│ └── showings/
|
||||||
|
├── manage.py
|
||||||
|
└── requirements.txt
|
||||||
|
|
||||||
|
## 三、核心 Model:租户与域名
|
||||||
|
|
||||||
|
- Company 继承 TenantMixin
|
||||||
|
- Domain 继承 DomainMixin
|
||||||
|
- 每一个中介公司 = 一个租户 = 一个独立 PostgreSQL Schema
|
||||||
|
- 每个公司可绑定多个域名/子域名
|
||||||
|
|
||||||
|
## 四、Settings 完整配置
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
- SHARED_APPS 放公共 Schema 应用
|
||||||
|
- TENANT_APPS 放租户私有应用
|
||||||
|
- TENANT_MODEL = "tenants.Company"
|
||||||
|
- TENANT_DOMAIN_MODEL = "tenants.Domain"
|
||||||
|
- DATABASES 使用 django_tenants.postgresql_backend
|
||||||
|
- DATABASE_ROUTERS 使用 TenantSyncRouter
|
||||||
|
- TenantMainMiddleware 必须第一个
|
||||||
|
- ROOT_URLCONF / PUBLIC_SCHEMA_URLCONF 分离
|
||||||
|
- AUTH_USER_MODEL = accounts.User
|
||||||
|
|
||||||
|
## 五、URL 路由拆分
|
||||||
|
|
||||||
|
- config/urls_public.py:公共域名、官网、注册、登录、超级管理后台
|
||||||
|
- config/urls_tenant.py:租户子域名、租户后台、房源/客源/带看/员工模块
|
||||||
|
|
||||||
|
## 六、自定义 User Model(跨租户关键)
|
||||||
|
|
||||||
|
- User 继承 AbstractUser
|
||||||
|
- role 支持平台超管、公司管理员、门店经理、资深经纪人、经纪人、实习经纪人
|
||||||
|
- Branch 作为门店模型
|
||||||
|
|
||||||
|
## 七、初始化与常用命令
|
||||||
|
|
||||||
|
- createdb realestate_saas
|
||||||
|
- python manage.py migrate_schemas --shared
|
||||||
|
- python manage.py createsuperuser
|
||||||
|
- python manage.py shell 创建 Company 与 Domain
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- schema_name = zuoan
|
||||||
|
- domain = zuoan.localhost
|
||||||
|
- 访问 http://zuoan.localhost:8000/admin/ 进入专属后台
|
||||||
|
|
||||||
|
## 八、本地开发配置(hosts 文件)
|
||||||
|
|
||||||
|
- 127.0.0.1 localhost
|
||||||
|
- 127.0.0.1 zuoan.localhost
|
||||||
|
- 127.0.0.1 lianhe.localhost
|
||||||
|
- 127.0.0.1 xincheng.localhost
|
||||||
|
|
||||||
|
开发环境要点:
|
||||||
|
- ALLOWED_HOSTS 包含 .localhost
|
||||||
|
- 本地不用 HTTPS
|
||||||
|
|
||||||
|
## 九、数据隔离验证
|
||||||
|
|
||||||
|
使用 schema_context 切换 schema,验证 Listing 等数据互相隔离。
|
||||||
|
|
||||||
|
## 下一步建议
|
||||||
|
|
||||||
|
推荐顺序:
|
||||||
|
1. 先做房源/客源/带看完整数据模型
|
||||||
|
2. 再做 Django Admin 深度定制(Jazzmin 主题)
|
||||||
|
3. 最后补三级权限体系(总部/门店/经纪人)
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 上海房产中介 SaaS 系统规划
|
||||||
|
|
||||||
|
## 一、多租户架构选型
|
||||||
|
|
||||||
|
Django 多租户有三种主流方案,针对这个场景推荐独立 Schema 方案,也就是基于 PostgreSQL Schema 隔离的 django-tenants。
|
||||||
|
|
||||||
|
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 共享 Schema | 每张表加 tenant_id 字段 | 简单,运维成本低 | 数据隔离风险高 | 小规模、低安全需求 |
|
||||||
|
| 独立 Schema | PostgreSQL Schema 隔离 | 隔离好,性能佳 | 稍复杂 | 中介 SaaS,推荐 |
|
||||||
|
| 独立数据库 | 每租户独立 DB | 最高隔离 | 运维成本极高 | 超高安全需求 |
|
||||||
|
|
||||||
|
推荐使用 django-tenants:
|
||||||
|
|
||||||
|
pip install django-tenants
|
||||||
|
|
||||||
|
## 二、整体系统模块规划
|
||||||
|
|
||||||
|
SaaS 平台层:
|
||||||
|
- 租户注册
|
||||||
|
- 套餐管理
|
||||||
|
- 计费
|
||||||
|
- 超级后台
|
||||||
|
|
||||||
|
各中介公司 Tenant:
|
||||||
|
- 房源管理
|
||||||
|
- 客源管理
|
||||||
|
- 员工 / 权限管理
|
||||||
|
- 带看管理
|
||||||
|
- 合同管理
|
||||||
|
- 报表 / BI 看板
|
||||||
|
- 渠道推广
|
||||||
|
- 财务佣金
|
||||||
|
- 消息 / 通知中心
|
||||||
|
|
||||||
|
核心 Django App 拆分:
|
||||||
|
- tenants:租户管理(公共 Schema)
|
||||||
|
- accounts:用户 / 员工 / 角色权限
|
||||||
|
- listings:房源管理(核心)
|
||||||
|
- clients:客源 / 客户跟进
|
||||||
|
- showings:带看记录
|
||||||
|
- contracts:合同管理
|
||||||
|
- commissions:佣金 / 财务
|
||||||
|
- reports:数据报表
|
||||||
|
- notifications:消息通知
|
||||||
|
- subscription:套餐订阅(公共 Schema)
|
||||||
|
|
||||||
|
## 三、房源模块数据模型(核心)
|
||||||
|
|
||||||
|
Listing 关键字段包括:
|
||||||
|
- 基础信息:title、listing_type、status
|
||||||
|
- 上海地址结构:district、street、community、building、floor、unit
|
||||||
|
- 房屋属性:area、inner_area、layout、orientation、decoration、floor_total
|
||||||
|
- 价格:price、price_unit
|
||||||
|
- 归属:agent、source、exclusive
|
||||||
|
- 证件:certificate_no
|
||||||
|
- 时间:created_at、updated_at
|
||||||
|
|
||||||
|
## 四、技术栈推荐
|
||||||
|
|
||||||
|
后端:
|
||||||
|
- Django 5.x + DRF
|
||||||
|
- django-tenants
|
||||||
|
- django-guardian
|
||||||
|
- celery + redis
|
||||||
|
- django-filter
|
||||||
|
- PostgreSQL 15+
|
||||||
|
|
||||||
|
前端路径:
|
||||||
|
- 路径 A:Django Admin + 定制,最快上手,适合 MVP
|
||||||
|
- 路径 B:HTMX + Alpine.js + Tailwind,推荐中期方案
|
||||||
|
- 路径 C:Vue3 / React + DRF,长期推荐
|
||||||
|
|
||||||
|
建议:先 A,再 B,最后 C。
|
||||||
|
|
||||||
|
## 五、租户路由设计
|
||||||
|
|
||||||
|
通过子域名区分租户:
|
||||||
|
- company-a.yourapp.com
|
||||||
|
- company-b.yourapp.com
|
||||||
|
- admin.yourapp.com
|
||||||
|
|
||||||
|
核心设置:
|
||||||
|
- TENANT_MODEL = "tenants.Company"
|
||||||
|
- TENANT_DOMAIN_MODEL = "tenants.Domain"
|
||||||
|
- SHARED_APPS 放公共应用
|
||||||
|
- TENANT_APPS 放租户私有应用
|
||||||
|
|
||||||
|
## 六、开发阶段规划
|
||||||
|
|
||||||
|
Phase 1(1-2 月)MVP:
|
||||||
|
- 租户注册 / 登录
|
||||||
|
- 房源 CRUD + 图片上传
|
||||||
|
- 客源基础管理
|
||||||
|
- Django Admin 后台
|
||||||
|
|
||||||
|
Phase 2(2-3 月)核心业务:
|
||||||
|
- 带看流程
|
||||||
|
- 合同模板 + 生成
|
||||||
|
- 员工角色权限
|
||||||
|
- 基础报表
|
||||||
|
|
||||||
|
Phase 3(3-4 月)增长功能:
|
||||||
|
- 佣金结算
|
||||||
|
- 渠道推广(链家 / 安居客对接)
|
||||||
|
- 微信小程序端
|
||||||
|
- BI 数据看板
|
||||||
|
|
||||||
|
Phase 4 商业化:
|
||||||
|
- 套餐 / 计费系统
|
||||||
|
- 多城市扩展
|
||||||
|
|
||||||
|
## 七、优先推进建议
|
||||||
|
|
||||||
|
如果要最快落地,建议优先顺序:
|
||||||
|
1. django-tenants 完整配置
|
||||||
|
2. 房源 / 客源 / 带看数据模型
|
||||||
|
3. Django Admin 深度定制
|
||||||
|
4. 权限系统设计
|
||||||
|
5. 前端升级到 HTMX 或 Vue
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
title: django-tenants 完整配置指南
|
||||||
|
created: 2026-04-21
|
||||||
|
tags: [django, django-tenants, postgresql, saas, multi-tenant]
|
||||||
|
category: 技术笔记
|
||||||
|
---
|
||||||
|
|
||||||
|
# django-tenants 完整配置指南
|
||||||
|
|
||||||
|
## 一、安装依赖
|
||||||
|
|
||||||
|
pip install django-tenants psycopg2-binary django-jazzmin
|
||||||
|
|
||||||
|
## 二、项目目录结构
|
||||||
|
|
||||||
|
myproject/
|
||||||
|
├── config/
|
||||||
|
│ ├── settings/
|
||||||
|
│ │ ├── base.py
|
||||||
|
│ │ ├── development.py
|
||||||
|
│ │ └── production.py
|
||||||
|
│ ├── urls.py
|
||||||
|
│ └── wsgi.py
|
||||||
|
├── apps/
|
||||||
|
│ ├── tenants/
|
||||||
|
│ ├── subscription/
|
||||||
|
│ ├── accounts/
|
||||||
|
│ ├── listings/
|
||||||
|
│ ├── clients/
|
||||||
|
│ └── showings/
|
||||||
|
├── manage.py
|
||||||
|
└── requirements.txt
|
||||||
|
|
||||||
|
## 三、核心 Model:租户与域名
|
||||||
|
|
||||||
|
- Company 继承 TenantMixin
|
||||||
|
- Domain 继承 DomainMixin
|
||||||
|
- 每一个中介公司 = 一个租户 = 一个独立 PostgreSQL Schema
|
||||||
|
- 每个公司可绑定多个域名/子域名
|
||||||
|
|
||||||
|
## 四、Settings 完整配置
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
- SHARED_APPS 放公共 Schema 应用
|
||||||
|
- TENANT_APPS 放租户私有应用
|
||||||
|
- TENANT_MODEL = "tenants.Company"
|
||||||
|
- TENANT_DOMAIN_MODEL = "tenants.Domain"
|
||||||
|
- DATABASES 使用 django_tenants.postgresql_backend
|
||||||
|
- DATABASE_ROUTERS 使用 TenantSyncRouter
|
||||||
|
- TenantMainMiddleware 必须第一个
|
||||||
|
- ROOT_URLCONF / PUBLIC_SCHEMA_URLCONF 分离
|
||||||
|
- AUTH_USER_MODEL = accounts.User
|
||||||
|
|
||||||
|
## 五、URL 路由拆分
|
||||||
|
|
||||||
|
- config/urls_public.py:公共域名、官网、注册、登录、超级管理后台
|
||||||
|
- config/urls_tenant.py:租户子域名、租户后台、房源/客源/带看/员工模块
|
||||||
|
|
||||||
|
## 六、自定义 User Model(跨租户关键)
|
||||||
|
|
||||||
|
- User 继承 AbstractUser
|
||||||
|
- role 支持平台超管、公司管理员、门店经理、资深经纪人、经纪人、实习经纪人
|
||||||
|
- Branch 作为门店模型
|
||||||
|
|
||||||
|
## 七、初始化与常用命令
|
||||||
|
|
||||||
|
- createdb realestate_saas
|
||||||
|
- python manage.py migrate_schemas --shared
|
||||||
|
- python manage.py createsuperuser
|
||||||
|
- python manage.py shell 创建 Company 与 Domain
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- schema_name = zuoan
|
||||||
|
- domain = zuoan.localhost
|
||||||
|
- 访问 http://zuoan.localhost:8000/admin/ 进入专属后台
|
||||||
|
|
||||||
|
## 八、本地开发配置(hosts 文件)
|
||||||
|
|
||||||
|
- 127.0.0.1 localhost
|
||||||
|
- 127.0.0.1 zuoan.localhost
|
||||||
|
- 127.0.0.1 lianhe.localhost
|
||||||
|
- 127.0.0.1 xincheng.localhost
|
||||||
|
|
||||||
|
开发环境要点:
|
||||||
|
- ALLOWED_HOSTS 包含 .localhost
|
||||||
|
- 本地不用 HTTPS
|
||||||
|
|
||||||
|
## 九、数据隔离验证
|
||||||
|
|
||||||
|
使用 schema_context 切换 schema,验证 Listing 等数据互相隔离。
|
||||||
|
|
||||||
|
## 下一步建议
|
||||||
|
|
||||||
|
推荐顺序:
|
||||||
|
1. 先做房源/客源/带看完整数据模型
|
||||||
|
2. 再做 Django Admin 深度定制(Jazzmin 主题)
|
||||||
|
3. 最后补三级权限体系(总部/门店/经纪人)
|
||||||
|
|
||||||
63
Leo/科学/Gravity – 以物理为基础的太阳系模拟器.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: Gravity – 以物理为基础的太阳系模拟器|如果家里有喜欢问“为什么”的孩子,推荐收藏
|
||||||
|
source: https://www.appinn.com/gravity-the-solar-system/
|
||||||
|
author:
|
||||||
|
- "[[青小蛙]]"
|
||||||
|
published: 2026-06-14
|
||||||
|
created: 2026-06-19
|
||||||
|
description: Gravity 是一个开源的太阳系模拟器,通过 24 个交互式动画,让小朋友了解整个太阳系,包括什么是引力?引力构建太阳/地球、为什么地球不会掉进太阳?火箭如何发射才不会掉下来等问题。
|
||||||
|
tags:
|
||||||
|
---
|
||||||
|
孩子们总会问一些看似简单、却很难解释的问题:
|
||||||
|
|
||||||
|
- 为什么地球不会掉进太阳里?
|
||||||
|
- 月亮为什么不会掉到地球上?
|
||||||
|
- 太阳那么大,为什么不会把所有行星都吸过去?
|
||||||
|
- 火箭为什么能飞上太空?
|
||||||
|
- 为什么火箭飞上去之后不会掉下来?
|
||||||
|
|
||||||
|
如果干巴巴的直接解释,似乎毫无吸引力。
|
||||||
|
|
||||||
|
但如果配合这个可以动的 Gravity:
|
||||||
|
|
||||||
|
<iframe src="https://player.bilibili.com/player.html?isOutside=true&aid=116747495672662&bvid=BV11HJw6vEdc&cid=39110510533&p=1&autoplay=0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
|
||||||
|
|
||||||
|
就很赞了。
|
||||||
|
## Gravity:开源太阳系模拟器
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Gravity – 以物理为基础的太阳系模拟器|如果家里有喜欢问“为什么”的孩子,推荐收藏 1
|
||||||
|
|
||||||
|
Gravity 是一个开源的太阳系模拟器,通过 24 个交互式动画,让小朋友了解整个太阳系,包括什么是引力?引力构建太阳/地球、为什么地球不会掉进太阳?火箭如何发射才不会掉下来等问题。
|
||||||
|
|
||||||
|
|
||||||
|
注意:一切都由真实的观测数据驱动;渲染器唯一进行“伪造”的是 **比例** ,使用了 **SpaceX 风格 UI** 。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Gravity – 以物理为基础的太阳系模拟器|如果家里有喜欢问“为什么”的孩子,推荐收藏 2
|
||||||
|
|
||||||
|
1. **什么是引力?** (`#what-is-gravity`) —— 展示两个质量体及其之间相等且相反的力矢量(牛顿第三定律);同样的力,产生不等的效应。
|
||||||
|
2. **引力构建太阳** (`#birth-of-sun`) —— 尘埃云坍缩并旋转形成太阳(吸积动画)。
|
||||||
|
3. **引力构建地球** (`#birth-of-earth`) —— 在剩余的圆盘中发生同样的微观过程;初生的地球在形成时闪烁着熔融的光芒。
|
||||||
|
4. **运动的物体保持运动** (`#inertia`) —— 移除太阳;地球以恒定速度沿直线漂移(牛顿第一定律)。纯粹的惯性。
|
||||||
|
5. **为什么地球不会掉进太阳** (`#why-no-fall`) —— 速度矢量 + 引力矢量 + 虚线的“无引力直线路径”。引力将直线弯曲成闭合环 —— 轨道就是持续坠落并始终错过。 …(及其他 19 个步骤)
|
||||||
|
|
||||||
|
## 真实性说明
|
||||||
|
|
||||||
|
- **大小** —— 每个天体都使用其真实的平均半径(太阳 696,340 公里 → 冥王星 1,188 公里)和质量。
|
||||||
|
- **轨道** —— 使用来自 JPL/IAU 近似根数表的真实 J2000.0 日心开普勒根数(半长轴、离心率、倾角、升交点黄经、近日点黄经、平黄经)。每帧都会求解开普勒方程。
|
||||||
|
- **日期** —— 时钟是真实的:T=0 对应 J2000 历元(2000-01-01 12:00)。
|
||||||
|
|
||||||
|
最后,还有一个自由探索功能:
|
||||||
|
## 获取
|
||||||
|
|
||||||
|
- 直接用: [https://gravity.appinn.com](https://gravity.appinn.com/)
|
||||||
|
- 代码在 [GitHub](https://github.com/scavin/Gravity/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
原文:https://www.appinn.com/gravity-the-solar-system/
|
||||||
|
|
||||||
|
最后,青小蛙和小朋友一起看了这个 Gravity,没想到他居然耐着性子把 24 个问题看完了。
|
||||||
353
Project/fonrey/ADR.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# Fonrey ADR 动态决策记录(Architecture & Requirement Decision Records)
|
||||||
|
|
||||||
|
> 目的:沉淀 Vibe Coding 过程中的**技术决策**与**需求决策**,避免跨会话后方案漂移。
|
||||||
|
> 适用范围:全项目(TECH_STACK / DATA_MODEL / PRD / TEST_CASES / 开发流程)。
|
||||||
|
> 维护原则:新增决策只追加,不覆盖历史;若决策被替代,必须新增 supersede 记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 初始化 ADR 动态决策记录;补录当日关键技术与需求决策 |
|
||||||
|
| 2026-05-02 | Sisyphus | 新增 ADR-20260502-001:合并系统管理与客户端发布两份 PRD 为统一的『平台管理后台 PRD』,原文件删除 |
|
||||||
|
| 2026-05-02 | Sisyphus | 新增 ADR-20260502-003:定义『PRD 与 Tech 文档职责边界』规则(PRD 管 what/why、Tech 管 how);首次落地于登录管理 PRD v3.0 |
|
||||||
|
| 2026-06-04 | Sisyphus | 新增 ADR-20260604-001:列表分页全局采用 Keyset(cursor),允许 page-based 作过渡兼容;同步修订 `UI_DESIGN/ROUTES.md` 与 `UI_DESIGN/房源管理/房源列表_UI.md` |
|
||||||
|
| 2026-06-04 | Sisyphus | 新增 ADR-20260604-002:ROUTES.md 客源条目"重复客源(成交)"与"重复客源(公客)"合并为单条 `/clients/duplicates/?tab=transacted\|public`,客源条目数稳定为 19 |
|
||||||
|
|
||||||
|
## 一、记录规范(必须遵守)
|
||||||
|
|
||||||
|
### 1.1 决策ID规则
|
||||||
|
- 格式:`ADR-YYYYMMDD-XXX`
|
||||||
|
- 例:`ADR-20260430-001`
|
||||||
|
|
||||||
|
### 1.2 决策类型
|
||||||
|
- `TECH`:技术决策(架构、接口、数据、测试、流程规范)
|
||||||
|
- `REQ`:需求决策(范围、术语、优先级、验收口径)
|
||||||
|
|
||||||
|
### 1.3 状态枚举
|
||||||
|
- `accepted`:已生效
|
||||||
|
- `superseded`:已被替代(需指向新 ADR)
|
||||||
|
- `deprecated`:废弃(不再执行)
|
||||||
|
|
||||||
|
### 1.4 最小字段
|
||||||
|
每条 ADR 必须包含:
|
||||||
|
1. 决策ID
|
||||||
|
2. 日期
|
||||||
|
3. 模块
|
||||||
|
4. 类型(TECH/REQ)
|
||||||
|
5. 状态
|
||||||
|
6. 背景
|
||||||
|
7. 决策内容
|
||||||
|
8. 影响范围
|
||||||
|
9. 关联文档
|
||||||
|
10. 备注(可选)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、按日期记录(主索引)
|
||||||
|
|
||||||
|
## 2026-04-30
|
||||||
|
|
||||||
|
### ADR-20260430-001
|
||||||
|
- **类型**:REQ
|
||||||
|
- **模块**:测试治理(全局)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:测试文档与沟通中存在“业务旅程/测试用例”术语混用,易导致执行偏差。
|
||||||
|
- **决策**:全项目统一使用“测试用例”术语,不再以“业务旅程”作为执行口径。
|
||||||
|
- **影响范围**:`TECH_STACK/测试规范.md`、`TEST_CASES/*`
|
||||||
|
- **关联文档**:`TECH_STACK/测试规范.md`
|
||||||
|
|
||||||
|
### ADR-20260430-002
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:测试治理(全局)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:多模块并行时测试编号易重复、不可追溯。
|
||||||
|
- **决策**:采用全局唯一测试用例编号:`TC-FON-XXXXXX`,并建立注册表,禁止复用废弃编号。
|
||||||
|
- **影响范围**:所有测试用例文档与测试报告系统
|
||||||
|
- **关联文档**:
|
||||||
|
- `TEST_CASES/TEST_CASE_ID_SPEC.md`
|
||||||
|
- `TEST_CASES/TEST_CASE_REGISTRY.md`
|
||||||
|
|
||||||
|
### ADR-20260430-003
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:测试执行与报告
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:失败定位粒度不足,无法快速定位到具体步骤。
|
||||||
|
- **决策**:测试报告必须支持 `test_case_id + step_id` 级追踪,并记录 `expected/actual/error_message` 与证据路径。
|
||||||
|
- **影响范围**:CI 报告、自动化测试框架
|
||||||
|
- **关联文档**:`TECH_STACK/测试规范.md`
|
||||||
|
|
||||||
|
### ADR-20260430-004
|
||||||
|
- **类型**:REQ
|
||||||
|
- **模块**:登录管理
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:登录 PRD v2.0 与技术方案存在接口命名漂移。
|
||||||
|
- **决策**:登录模块接口路径以 PRD v2.0 为唯一权威,技术方案与测试用例全部对齐 PRD 路径。
|
||||||
|
- **影响范围**:登录模块 API 设计、自动化测试调用路径
|
||||||
|
- **关联文档**:
|
||||||
|
- `PRD/登录管理/用户登录管理模块PRD.md`
|
||||||
|
- `TECH_STACK/登录管理技术方案.md`
|
||||||
|
- `TEST_CASES/TEST_CASES_LOGIN_MODULE.md`
|
||||||
|
|
||||||
|
### ADR-20260430-005
|
||||||
|
- **类型**:REQ
|
||||||
|
- **模块**:登录管理
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:Story 边界存在历史遗留混淆。
|
||||||
|
- **决策**:登录模块范围冻结为:短信验证码登录为 MVP 正式能力;“找回用户名”废弃;微信扫码保留为预留。
|
||||||
|
- **影响范围**:实现范围、测试覆盖边界
|
||||||
|
- **关联文档**:
|
||||||
|
- `PRD/登录管理/用户登录管理模块PRD.md`
|
||||||
|
- `TECH_STACK/登录管理技术方案.md`
|
||||||
|
|
||||||
|
### ADR-20260430-006
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:客户端发布管理
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:发布模块缺少可执行技术方案,无法指导实现与测试。
|
||||||
|
- **决策**:新增独立技术方案文档,明确 Electron 壳层边界、EV 签名、Heartbeat、自动更新、完整性校验、R2 版本管理、官网分发、便携版策略。
|
||||||
|
- **影响范围**:`apps/release` 设计与落地
|
||||||
|
- **关联文档**:
|
||||||
|
- `TECH_STACK/客户端发布管理技术方案.md`
|
||||||
|
- `PRD/发布管理/客户端发布管理模块PRD.md`
|
||||||
|
|
||||||
|
### ADR-20260430-007
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:客户端发布管理 / 数据模型
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:统计口径需支撑跨租户版本分布与活跃安装数。
|
||||||
|
- **决策**:以 `public.client_heartbeats` 作为心跳事实表,使用 `(tenant_id, device_id)` 为 Upsert 锚点,活跃定义为最近 24h。
|
||||||
|
- **影响范围**:统计接口、看板与聚合查询
|
||||||
|
- **关联文档**:`DATA_MODEL/DATA_MODEL_PUBLIC.md`
|
||||||
|
|
||||||
|
### ADR-20260430-008
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:客户端发布管理 / 安全
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:下载链路存在篡改与损坏风险。
|
||||||
|
- **决策**:客户端更新包必须进行 SHA256 完整性校验,校验失败禁止安装并保留当前版本可用。
|
||||||
|
- **影响范围**:客户端更新器、发布接口响应字段
|
||||||
|
- **关联文档**:
|
||||||
|
- `TECH_STACK/客户端发布管理技术方案.md`
|
||||||
|
- `PRD/发布管理/客户端发布管理模块PRD.md`
|
||||||
|
|
||||||
|
### ADR-20260430-009
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:客户端发布管理 / API
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:历史文档存在 `/api/client/updates/*` 与 `/api/release/updates/*` 双口径。
|
||||||
|
- **决策**:发布模块统一采用 `/api/release/...` 命名空间。
|
||||||
|
- **影响范围**:服务端路由、客户端调用、测试用例
|
||||||
|
- **关联文档**:
|
||||||
|
- `PRD/发布管理/客户端发布管理模块PRD.md`
|
||||||
|
- `TECH_STACK/TECH_STACK.md`
|
||||||
|
- `TECH_STACK/客户端发布管理技术方案.md`
|
||||||
|
|
||||||
|
### ADR-20260430-010
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:文档治理(全局)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:多目录文档缺少统一变更追溯入口。
|
||||||
|
- **决策**:`TECH_STACK / DATA_MODEL / TEST_CASES` 下文档统一添加“变更历史”章节,并规范位置:**头部元信息之后、正文主章节(## 一/## 1)之前**。
|
||||||
|
- **影响范围**:全量技术文档与测试文档
|
||||||
|
- **关联文档**:
|
||||||
|
- `TECH_STACK/*.md`
|
||||||
|
- `DATA_MODEL/*.md`
|
||||||
|
- `TEST_CASES/*.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-02
|
||||||
|
|
||||||
|
### ADR-20260502-001
|
||||||
|
- **类型**:REQ
|
||||||
|
- **模块**:平台管理后台(PRD 治理)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:原 `PRD/系统管理/系统管理模块PRD.md`(v1.3)与 `PRD/发布管理/客户端发布管理模块PRD.md`(v1.2)虽分别归口「系统管理」与「客户端发布」,但二者实际受众均为**平台管理员**(Platform Admin / 运营人员 / 只读审计员),分别描述会导致:① 页面路由与权限矩阵在两份 PRD 中各自演化、容易割裂;② API 操作清单缺乏统一规划;③ 客户端发布 v1.1 已明确归属"平台运营后台",与系统管理同处一域。
|
||||||
|
- **决策**:
|
||||||
|
1. 合并为单一文档 `PRD/平台管理后台/平台管理后台PRD.md`(v1.0),定位「面向平台管理员的统一后台 PRD」;以产品视角统一规划页面清单、访问权限、页面间导航逻辑、业务 API 操作清单(不绑定具体路径)。
|
||||||
|
2. 原两份 PRD 文件**直接删除**,由新文档完全取代;新文档头部声明 supersede 关系。
|
||||||
|
3. 客户端运行时与平台之间的接口(如查询最新版本、上报心跳)维持在 `TECH_STACK/客户端发布管理技术方案.md` 中描述,本 PRD 仅描述平台管理员可执行的业务操作。
|
||||||
|
4. 角色权限矩阵、租户状态机、客户端版本治理(含全平台租户活跃榜)等内容统一并入新 PRD 第 5–7 章。
|
||||||
|
- **影响范围**:
|
||||||
|
- PRD:删除 `PRD/系统管理/系统管理模块PRD.md`、`PRD/发布管理/客户端发布管理模块PRD.md`;新增 `PRD/平台管理后台/平台管理后台PRD.md`
|
||||||
|
- README:模块入口索引同步更新,原「系统管理」「客户端发布」入口指向新 PRD(客户端技术方案文档保留)
|
||||||
|
- TECH_STACK:`客户端发布管理技术方案.md` 关联 PRD 的引用需同步指向新 PRD
|
||||||
|
- **关联文档**:
|
||||||
|
- `PRD/平台管理后台/平台管理后台PRD.md`
|
||||||
|
- `README.md`
|
||||||
|
- `TECH_STACK/客户端发布管理技术方案.md`
|
||||||
|
- `TECH_STACK/系统设置技术方案.md`
|
||||||
|
- **备注**:本决策不改变任何技术口径(API 命名空间、`client_heartbeats` 表结构、SHA256 校验等),仅是 PRD 文档治理层面的合并与归属调整。`ADR-20260430-006/007/008/009` 全部继续生效。
|
||||||
|
|
||||||
|
### ADR-20260502-002
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:平台管理后台(TECH_STACK 治理)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:随 `ADR-20260502-001` 完成 PRD 合并后,TECH_STACK 域仍存在两份独立技术方案:`TECH_STACK/客户端发布管理技术方案.md`(v1.0)与 `TECH_STACK/系统管理技术文档.md`(v1.x)。两者受众一致(平台管理员后台),却分别演化路由表、API 命名空间、Mixin 守卫链与错误码,存在以下风险:① 路由守卫与 step-up MFA 协议在两文档中各自定义,易割裂;② API 命名空间(`/admin/...` 后台 vs `/api/release/...` 客户端运行时)缺乏统一约束;③ Celery 队列、Redis Key、错误码族无法横向对齐。
|
||||||
|
- **决策**:
|
||||||
|
1. 合并为单一文档 `TECH_STACK/平台管理后台技术方案.md`(v1.0),覆盖 PRD 要求的三维度——**技术选型**(实现路由与 API 的框架)、**页面路由表**(路径定义、动态参数、路由守卫、懒加载)、**API 设计**(RESTful 路径、HTTP 方法、请求/响应、错误码、版本控制、认证方式)。
|
||||||
|
2. 原 `TECH_STACK/客户端发布管理技术方案.md` 与 `TECH_STACK/系统管理技术文档.md` **直接删除**,由新文档完全取代。
|
||||||
|
3. 双 API 命名空间最终落地:`/admin/...`(HTMX Partial,Session+CSRF+TOTP,无路径版本号)、`/admin/api/client-releases/...`(管理端 JSON 写操作)、`/api/release/v1/...`(客户端运行时,路径版本号 `v1`)。
|
||||||
|
4. App 拆分:`apps/admin_console`(租户/备份/导出/升级/审计/管理员/Feature Flag)+ `apps/release`(客户端发布 + 客户端运行时 API),后者仅依赖前者的 `permissions / middleware / services.audit_service`。
|
||||||
|
5. `TECH_STACK/TECH_STACK.md` §8 模块表合并「系统设置」「客户端发布」相关行为单行「平台管理后台」入口;README 模块入口同步指向新文档。
|
||||||
|
- **影响范围**:
|
||||||
|
- TECH_STACK:新增 `平台管理后台技术方案.md`;删除 `客户端发布管理技术方案.md`、`系统管理技术文档.md`
|
||||||
|
- TECH_STACK:`TECH_STACK.md` §8 模块表
|
||||||
|
- README:模块入口索引(平台管理后台一行)
|
||||||
|
- **关联文档**:
|
||||||
|
- `TECH_STACK/平台管理后台技术方案.md`
|
||||||
|
- `TECH_STACK/TECH_STACK.md`
|
||||||
|
- `PRD/平台管理后台/平台管理后台PRD.md`
|
||||||
|
- `ADR-20260502-001`、`ADR-20260430-006/007/008/009`
|
||||||
|
- **备注**:本决策不变更任何技术口径(API 命名空间、`/api/release/v1/...` 版本号、SHA256 校验、`client_heartbeats` Upsert + 24h 活跃口径、审计不可变约束等),仅在 TECH_STACK 文档治理层面执行合并与归属调整。
|
||||||
|
|
||||||
|
### ADR-20260502-003
|
||||||
|
- **类型**:REQ
|
||||||
|
- **模块**:文档治理(全局)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:历史 PRD(如 `PRD/登录管理/用户登录管理模块PRD.md` v2.0、`PRD/平台管理后台/平台管理后台PRD.md` v1.0)混杂大量实现细节——具体 API 路径与 HTTP 方法、Redis Key 格式与 TTL、Django 字段类型与中间件类名、Electron API 名、CSS 类名、Cookie 属性等。这导致:① PRD 评审被技术细节淹没,业务边界讨论失焦;② 实现口径出现"PRD 写一份、Tech 写一份"的双源头,长期产生漂移(参见 `ADR-20260430-004` 已为登录模块单独修过一次);③ 业务变更与技术调整混在同一份文档里,变更范围与责任人难以拆分。
|
||||||
|
- **决策**:自本 ADR 起,全项目 PRD 与 Tech 文档严格遵循以下职责边界,任何新建/修订必须自检通过。
|
||||||
|
|
||||||
|
**PRD 应包含("是什么 / 为什么")**:
|
||||||
|
1. **页面与导航**:列出页面清单、访问权限(角色矩阵)、用户视角的页面间跳转逻辑、登录态/未登录态分支。✅ "/dashboard 需登录后访问";✅ "用户点击『详情』后跳转到订单详情页"。
|
||||||
|
2. **业务操作清单**:以业务动词描述用户/角色可执行的能力。✅ "用户可查询自己的订单列表";✅ "管理员可批量更新商品状态"。
|
||||||
|
3. **业务规则与数据约束**:抽象层面的规则与数值阈值(如『密码连续错误 5 次锁定 30 分钟』『短信验证码有效期 10 分钟』),但仅以业务语言表达,不绑定 Redis Key 或字段名。
|
||||||
|
4. **状态机**:业务状态枚举与合法跃迁。
|
||||||
|
5. **验收标准**:可由产品/QA 验证的用户可见行为。
|
||||||
|
|
||||||
|
**PRD 必须移出(移交 Tech / DATA_MODEL)**:
|
||||||
|
1. ❌ 具体 API 路径(如 `POST /api/auth/login/phone/`)与 HTTP 方法。
|
||||||
|
2. ❌ 请求/响应 JSON Schema、字段名、错误码常量。
|
||||||
|
3. ❌ Redis Key 格式、TTL、缓存策略、消息队列名。
|
||||||
|
4. ❌ 数据库字段名、字段类型(`CharField(30)`、`OneToOneField`)、表名、索引名、中间件类名。
|
||||||
|
5. ❌ 前端框架/库的 API 名(`BrowserWindow.loadURL`、`electron-store`、`HX-Request` 头)、CSS 类名、Cookie 属性(`SameSite=Strict`、`HttpOnly`)。
|
||||||
|
6. ❌ 实现选型与库依赖清单(除非业务上明确强制,如"必须由具备短信资质的服务商发送")。
|
||||||
|
|
||||||
|
**PRD 引用 Tech 的标准格式**:当业务规则需要技术细节落地时,PRD 用以下任一方式引用:
|
||||||
|
- "(实现细节详见 `TECH_STACK/<模块>技术方案.md` §X.Y)"
|
||||||
|
- "(数据结构见 `DATA_MODEL/<模块>.md`)"
|
||||||
|
- "(端点契约见 `TECH_STACK/API_CONTRACT.md`)"
|
||||||
|
|
||||||
|
**Tech 文档应承接**:上述被移出的全部内容;若 PRD 移出某项时 Tech 文档尚未承接,**必须在同一次提交内同步补齐 Tech**,禁止信息丢失。
|
||||||
|
|
||||||
|
**测试用例不受本 ADR 约束**:测试用例本质上需要可执行的实现细节(路径、字段、错误码),保留细节不动;但其引用 PRD 章节时应同步更新章节号。
|
||||||
|
|
||||||
|
- **影响范围**:
|
||||||
|
- PRD:所有 PRD 文档需按本规则审视并修订(首批落地:登录管理 PRD → v3.0;后续模块按需推进)
|
||||||
|
- TECH_STACK:所有 Tech 文档需承接对应实现细节,缺口必须同步补齐
|
||||||
|
- 未来所有新建 PRD:必须自检本规则
|
||||||
|
- CI/Review:建议在 PR Review checklist 增加"PRD 是否含具体 API 路径/Redis Key/字段类型"的反向检查项
|
||||||
|
- **关联文档**:
|
||||||
|
- `AGENTS.md` §9.1(ADR 治理联动规则)
|
||||||
|
- `PRD/登录管理/用户登录管理模块PRD.md`(v3.0 首批落地)
|
||||||
|
- `TECH_STACK/登录管理技术方案.md`(同步承接被移出细节)
|
||||||
|
- `ADR-20260430-004`(登录接口路径以 PRD 为准 → 本 ADR 后语义升级为:业务能力以 PRD 为准,具体路径以 Tech 为准;不冲突,互补)
|
||||||
|
- **备注**:本 ADR 不更改任何已实现的技术口径,仅约束**文档承载位置**。`ADR-20260430-004` 关于"登录接口最终路径"的权威源仍以两份文档同步为准(PRD 描述业务操作、Tech 描述具体路径),二者不应再次出现漂移。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-04
|
||||||
|
|
||||||
|
### ADR-20260604-001
|
||||||
|
- **类型**:TECH
|
||||||
|
- **模块**:列表分页(全局)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:`AGENTS.md` §4.5 与 `UI_DESIGN/ROUTES.md` §1 全局约定明文要求"所有列表查询必须使用 Keyset 分页(参数禁止包含 `offset`,统一使用 `cursor`)"。但 `UI_DESIGN/房源管理/房源列表_UI.md` 现存示例(行 97 query params 列表、行 741 HTMX `hx-vals='{"page": N}'`)仍是 page-based 风格,与全局规范冲突。MVP 阶段一次性切到 Keyset 会拖慢前端模板与示例代码改造节奏,且页码 UI(`[1][2][3]...[N]`)已被多份截图与组件清单采用。
|
||||||
|
- **决策**:
|
||||||
|
1. **目标口径**:列表分页长期统一为 Keyset(cursor);新增列表页与新增分页 API 不得使用 page/offset。
|
||||||
|
2. **过渡兼容**:MVP Phase 1 期间,已有 page-based 示例的模块文档与代码可保留 page UI 表现(页码导航 + 跳页),但**数据获取层必须支持 cursor 参数**;后端 API 接受 `cursor` 为主参数,`page` 仅作过渡兼容输入且不得用于 1000 条以上数据集。
|
||||||
|
3. **文档承接**:`UI_DESIGN/ROUTES.md` §1 通用约定新增"过渡兼容"说明;`UI_DESIGN/房源管理/房源列表_UI.md` 示例代码改为 `cursor`,分页 UI 表现保留。
|
||||||
|
4. **退出条件**:MVP GA 后 1 个版本内,所有 page-based 示例必须清理;届时新增 ADR 关闭本过渡条款。
|
||||||
|
- **影响范围**:
|
||||||
|
- `UI_DESIGN/ROUTES.md` §1
|
||||||
|
- `UI_DESIGN/房源管理/房源列表_UI.md` §2.1.1、§2.1.3 分页栏
|
||||||
|
- 后续所有列表页 HTMX 模板与 API 视图
|
||||||
|
- **关联文档**:
|
||||||
|
- `AGENTS.md` §4.5
|
||||||
|
- `UI_DESIGN/ROUTES.md`
|
||||||
|
- `UI_DESIGN/房源管理/房源列表_UI.md`
|
||||||
|
- **备注**:B-04(Keyset 分页规范缺位)由本 ADR 部分关闭;剩余的列表索引矩阵补全留待后续 Major 修订。
|
||||||
|
|
||||||
|
### ADR-20260604-002
|
||||||
|
- **类型**:REQ
|
||||||
|
- **模块**:客源管理(路由汇总)
|
||||||
|
- **状态**:accepted
|
||||||
|
- **背景**:`UI_DESIGN/ROUTES.md` §5 客源条目数与历史 handoff 口径"客源 19 项"不一致——实际列出 20 行,多出的 1 行来自将"重复客源(成交)"与"重复客源(公客)"拆分为两条独立路由。两者复用同一列表页结构、同一权限范围、同一组件清单,仅业务子集不同,符合 ROUTES.md 全局约定中"Tab 维度统一用 `?tab=` 表达"的模式。
|
||||||
|
- **决策**:将两条重复客源路由合并为单条 `/clients/duplicates/`,子集通过 `?tab=transacted|public` 区分;客源条目总数稳定为 19。
|
||||||
|
- **影响范围**:
|
||||||
|
- `UI_DESIGN/ROUTES.md` §5(合并 2 行为 1 行)
|
||||||
|
- **关联文档**:
|
||||||
|
- `UI_DESIGN/ROUTES.md`
|
||||||
|
- `UI_DESIGN/客源管理/客源列表_UI.md`
|
||||||
|
- **备注**:本 ADR 不变更任何业务规则,仅统一 URL 表达形式与条目计数。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、按模块分类记录(视图索引)
|
||||||
|
|
||||||
|
## 3.1 测试治理(全局)
|
||||||
|
- `ADR-20260430-001`:术语统一为“测试用例”(REQ)
|
||||||
|
- `ADR-20260430-002`:全局唯一测试编号机制(TECH)
|
||||||
|
- `ADR-20260430-003`:报告粒度到 step_id(TECH)
|
||||||
|
|
||||||
|
## 3.2 登录管理
|
||||||
|
- `ADR-20260430-004`:接口路径以 PRD 为准(REQ)
|
||||||
|
- `ADR-20260430-005`:MVP 范围冻结(REQ)
|
||||||
|
|
||||||
|
## 3.3 客户端发布管理
|
||||||
|
- `ADR-20260430-006`:新增独立技术方案(TECH) *(superseded by `ADR-20260502-002`,技术方案已合并至『平台管理后台技术方案』)*
|
||||||
|
- `ADR-20260430-008`:SHA256 完整性校验强制(TECH)
|
||||||
|
- `ADR-20260430-009`:API 命名空间统一 `/api/release/...`(TECH)
|
||||||
|
|
||||||
|
## 3.4 数据模型(public schema)
|
||||||
|
- `ADR-20260430-007`:`client_heartbeats` Upsert + 24h 活跃口径(TECH)
|
||||||
|
|
||||||
|
## 3.5 文档治理(全局)
|
||||||
|
- `ADR-20260430-010`:变更历史章节统一规则(TECH)
|
||||||
|
- `ADR-20260502-003`:PRD 与 Tech 文档职责边界(REQ)
|
||||||
|
|
||||||
|
## 3.6 平台管理后台
|
||||||
|
- `ADR-20260502-001`:合并系统管理 PRD 与客户端发布 PRD 为统一的『平台管理后台 PRD』(REQ)
|
||||||
|
- `ADR-20260502-002`:合并系统管理技术文档与客户端发布技术方案为统一的『平台管理后台技术方案』(TECH)
|
||||||
|
|
||||||
|
## 3.7 列表分页(全局)
|
||||||
|
- `ADR-20260604-001`:Keyset(cursor) 为目标口径;MVP 期允许 page-based 作过渡兼容(TECH)
|
||||||
|
|
||||||
|
## 3.8 客源管理(路由)
|
||||||
|
- `ADR-20260604-002`:重复客源合并为单条 `/clients/duplicates/?tab=transacted|public`,客源条目数=19(REQ)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、历史记录(Append-Only Log)
|
||||||
|
|
||||||
|
> 说明:本节为机器可检索的历史流水,不删改旧记录。
|
||||||
|
|
||||||
|
| ADR ID | 日期 | 模块 | 类型 | 状态 | 摘要 | 关联 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| ADR-20260430-001 | 2026-04-30 | 测试治理 | REQ | accepted | 术语统一为“测试用例” | `TECH_STACK/测试规范.md` |
|
||||||
|
| ADR-20260430-002 | 2026-04-30 | 测试治理 | TECH | accepted | 测试编号全局唯一 `TC-FON-XXXXXX` | `TEST_CASES/TEST_CASE_ID_SPEC.md` |
|
||||||
|
| ADR-20260430-003 | 2026-04-30 | 测试执行 | TECH | accepted | 报告必须定位到 `test_case_id+step_id` | `TECH_STACK/测试规范.md` |
|
||||||
|
| ADR-20260430-004 | 2026-04-30 | 登录管理 | REQ | accepted | 登录接口路径全部按 PRD v2.0 | `TECH_STACK/登录管理技术方案.md` |
|
||||||
|
| ADR-20260430-005 | 2026-04-30 | 登录管理 | REQ | accepted | 登录模块范围冻结(含废弃/预留) | `PRD/登录管理/用户登录管理模块PRD.md` |
|
||||||
|
| ADR-20260430-006 | 2026-04-30 | 客户端发布 | TECH | accepted | 新建独立技术方案文档 | `TECH_STACK/客户端发布管理技术方案.md` |
|
||||||
|
| ADR-20260430-007 | 2026-04-30 | 数据模型 | TECH | accepted | `client_heartbeats` Upsert + 24h 活跃 | `DATA_MODEL/DATA_MODEL_PUBLIC.md` |
|
||||||
|
| ADR-20260430-008 | 2026-04-30 | 客户端发布安全 | TECH | accepted | SHA256 校验失败禁止安装 | `TECH_STACK/客户端发布管理技术方案.md` |
|
||||||
|
| ADR-20260430-009 | 2026-04-30 | 客户端发布 API | TECH | accepted | 统一 `/api/release/...` 路径 | `TECH_STACK/TECH_STACK.md` |
|
||||||
|
| ADR-20260430-010 | 2026-04-30 | 文档治理 | TECH | accepted | 变更历史章节位置统一规范 | `TECH_STACK/*.md` `DATA_MODEL/*.md` `TEST_CASES/*.md` |
|
||||||
|
| ADR-20260502-001 | 2026-05-02 | 平台管理后台 | REQ | accepted | 合并系统管理 PRD + 客户端发布 PRD 为『平台管理后台 PRD』,原文件删除 | `PRD/平台管理后台/平台管理后台PRD.md` |
|
||||||
|
| ADR-20260502-002 | 2026-05-02 | 平台管理后台 | TECH | accepted | 合并系统管理技术文档 + 客户端发布技术方案为『平台管理后台技术方案』(覆盖技术选型/页面路由表/API 设计三维度),原文件删除 | `TECH_STACK/平台管理后台技术方案.md` |
|
||||||
|
| ADR-20260502-003 | 2026-05-02 | 文档治理 | REQ | accepted | PRD 管 what/why、Tech 管 how;PRD 必须移出 API 路径/Redis Key/字段类型/框架 API 等实现细节,由 Tech 与 DATA_MODEL 承接 | `PRD/登录管理/用户登录管理模块PRD.md` v3.0 |
|
||||||
|
| ADR-20260604-001 | 2026-06-04 | 列表分页(全局) | TECH | accepted | Keyset(cursor) 为目标口径;MVP 期允许 page-based 作过渡兼容 | `UI_DESIGN/ROUTES.md`、`UI_DESIGN/房源管理/房源列表_UI.md` |
|
||||||
|
| ADR-20260604-002 | 2026-06-04 | 客源管理(路由) | REQ | accepted | 重复客源合并为单条 `/clients/duplicates/?tab=transacted\|public`,客源条目数=19 | `UI_DESIGN/ROUTES.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、后续维护约定
|
||||||
|
|
||||||
|
1. 每当接受新技术或需求决策,**当天必须新增 ADR 条目**。
|
||||||
|
2. 若新决策替代旧决策,在新 ADR 中写明 `supersedes: ADR-xxxx`,并将旧条目标记 `superseded`。
|
||||||
|
3. PRD / TECH_STACK / DATA_MODEL / TEST_CASES 发生关键口径变化时,必须同步更新本文件。
|
||||||
|
4. 本文件是跨 Session 的决策权威输入之一,和 PRD 一起喂给 Agent。
|
||||||
324
Project/fonrey/AGENTS.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any 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.
|
||||||
|
|
||||||
|
# Fonrey(房睿)— AGENTS.md
|
||||||
|
|
||||||
|
**适用对象**:所有 AI Coding Agent(vibe coding 模式)
|
||||||
|
**文档定位**:开发启动前的强制阅读清单,定义架构约定、禁止项和文档导航
|
||||||
|
**最后更新**:2026-04-27
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目概览
|
||||||
|
|
||||||
|
**Fonrey(房睿)房产经纪管理系统** —— 面向房地产经纪公司的 B2B SaaS 平台,解决房源/客源信息散乱、跟进缺失、重复录入等痛点,支撑单租户 89,000+ 房源数据量级下的高效匹配。
|
||||||
|
|
||||||
|
- **核心模块**:房源管理、客源管理、楼盘管理、组织人事、权限管理、登录管理、系统设置、客户端发布
|
||||||
|
- **目标用户**:一线经纪人(高频)、店长/经理(每日)、运营/行政(每日)、系统管理员(不定期)
|
||||||
|
- **形态**:Web 端为主 + Electron 桌面客户端(壳应用);移动端为 v2 规划
|
||||||
|
- **设计哲学**:数据一致性 > 录入/筛选速度 > UI 简洁高效。优先保障多租户数据物理隔离与极速响应。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术选型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Frontend** | HTMX + Alpine.js + Tailwind CSS | 无重前端框架;HTMX 局刷、Alpine 管状态、Tailwind 样式 |
|
||||||
|
| **Backend** | Django 4.x(ASGI 模式) | 支持异步能力 |
|
||||||
|
| **Multi-tenant** | `django-tenants` | PostgreSQL Schema 隔离,租户数据物理安全 |
|
||||||
|
| **Database** | PostgreSQL 16 + PgBouncer | 连接池优化,支撑高并发 |
|
||||||
|
| **Cache** | Redis | 缓存、限流、Token、权限快照 |
|
||||||
|
| **Tasks** | Celery + Celery Beat | 异步导出、智能配房、邮件、图片转码 |
|
||||||
|
| **Storage** | Cloudflare R2(S3 兼容) | 房源图片、附件、客户端安装包 |
|
||||||
|
| **CDN** | Cloudflare | 静态资源 + 客户端更新包加速 |
|
||||||
|
| **Server** | Gunicorn + Uvicorn workers + Nginx | ASGI 服务部署 |
|
||||||
|
| **Monitoring** | Sentry + Grafana | 错误追踪 + 指标监控 |
|
||||||
|
| **Deployment** | Docker Compose | 容器化部署 |
|
||||||
|
| **Desktop Client** | Electron + electron-updater | 壳应用,渲染层复用 Web 技术栈 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 目录结构
|
||||||
|
|
||||||
|
代码库标准目录结构如下,**严格遵循**,不得自行创建新的顶层目录:
|
||||||
|
|
||||||
|
```
|
||||||
|
fonrey/
|
||||||
|
├── apps/
|
||||||
|
│ ├── tenant/ # django-tenants 配置(shared_apps)
|
||||||
|
│ ├── account/ # 登录认证
|
||||||
|
│ ├── permission/ # 权限管理
|
||||||
|
│ ├── org/ # 组织人事(org_units, staff)
|
||||||
|
│ ├── region/ # 区域管理(districts, business_areas)
|
||||||
|
│ ├── complex/ # 楼盘管理(complexes, buildings, schools)
|
||||||
|
│ ├── property/ # 房源核心(含 models/services/tasks 三层)
|
||||||
|
│ ├── client/ # 客源管理
|
||||||
|
│ ├── setting/ # 系统设置(lookup, tags)
|
||||||
|
│ └── release/ # 客户端发布管理(shared_apps)
|
||||||
|
├── shared/ # 公共 Schema App(public schema 模型)
|
||||||
|
└── core/
|
||||||
|
├── models/base.py # 抽象基类(所有业务表继承)
|
||||||
|
├── encryption.py # PII 加密(AES-256-GCM)
|
||||||
|
└── cache.py # Redis 工具(含 tenant 前缀)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Django App 内部分层规范**(以 `property` 为典型,所有 App 参照执行):
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/property/
|
||||||
|
├── models/ # 一表一文件,禁止全部堆在 models.py
|
||||||
|
├── services/ # 业务逻辑(完成度计算、重复检测、匹配等)
|
||||||
|
├── tasks.py # Celery 异步任务
|
||||||
|
├── views.py # HTMX/JSON 视图(薄视图,调 services)
|
||||||
|
└── urls.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 关键开发约定
|
||||||
|
|
||||||
|
### 4.1 多租户隔离(最高优先级)
|
||||||
|
|
||||||
|
- 所有数据库查询**必须**基于当前租户 Schema;`django-tenants` 中间件已自动切换,不得绕过
|
||||||
|
- **严禁**跨租户 SQL 查询(包括 raw SQL 和 ORM 的 `using()` 指定 public schema)
|
||||||
|
- `shared_apps` 仅放平台基础数据:`Tenant`、`ClientRelease`、`PermissionDef`、`PlatformAdmin` 等
|
||||||
|
- Celery 任务必须在任务参数中传入 `tenant_schema_name`,任务开头调用 `connection.set_schema(schema_name)` 切换到正确 Schema,**不得依赖外部上下文传递**
|
||||||
|
|
||||||
|
### 4.2 前端交互约定
|
||||||
|
|
||||||
|
- **HTMX** 处理局部 DOM 刷新:分页、筛选、联想搜索、表单提交
|
||||||
|
- **Alpine.js** 处理前端状态:弹窗开关、多选状态、字数统计、条件显示
|
||||||
|
- **禁止**编写复杂原生 JavaScript;逻辑无法用 HTMX/Alpine 覆盖时,优先提出问题而不是自行引入 JS 库
|
||||||
|
- HTMX 请求失败(4xx/5xx)必须触发全局 Toast 提示(通过 `HX-Trigger` 响应头)
|
||||||
|
- 所有 HTMX 局部请求后端视图必须校验 `HX-Request` header,防止直接访问返回残缺 HTML
|
||||||
|
|
||||||
|
### 4.3 异步任务约定
|
||||||
|
|
||||||
|
- 所有耗时 **> 500ms** 的操作**必须**经 Celery 异步执行:Excel 导出、图片处理、智能配房、邮件发送、完成度重算
|
||||||
|
- Celery Beat 定时任务:私客自动转公客(每小时)、重复客源检测(每日)
|
||||||
|
- 任务状态存 Redis,前端通过轮询 HTMX 端点展示进度
|
||||||
|
|
||||||
|
### 4.4 数据模型约定
|
||||||
|
|
||||||
|
| 约定 | 规则 |
|
||||||
|
|------|------|
|
||||||
|
| 主键类型 | `UUID v4`(`gen_random_uuid()`),禁止自增整数 |
|
||||||
|
| 软删除 | 所有核心表含 `deleted_at TIMESTAMPTZ`,查询默认加 `WHERE deleted_at IS NULL` |
|
||||||
|
| 时间戳 | 全部使用 `TIMESTAMPTZ`(含时区),禁止 naive datetime |
|
||||||
|
| 手机号存储 | AES-256-GCM 加密,建立 SHA-256 哈希索引,通过 `core/encryption.py` 操作 |
|
||||||
|
| 审计字段 | `created_by UUID`、`updated_by UUID` 全表覆盖 |
|
||||||
|
| 枚举值 | 业务枚举用 `VARCHAR + CHECK CONSTRAINT`;不得在代码中硬编码中文枚举值 |
|
||||||
|
| 金额 | `NUMERIC(12,2)` 万元精度,禁止 FLOAT |
|
||||||
|
| 大文本 | `TEXT` 类型,不设长度限制 |
|
||||||
|
| 不可删除记录 | `listing_histories`、`price_changes` 无 `deleted_at`,append-only,禁止物理删除 |
|
||||||
|
|
||||||
|
### 4.5 查询性能约定
|
||||||
|
|
||||||
|
- 列表查询目标:89,000 条房源 / 200 万条跟进日志下,p95 响应 **< 2 秒**
|
||||||
|
- **所有列表查询必须使用 Keyset 分页**(`WHERE id > :last_id ORDER BY id`),禁止 OFFSET 分页用于大数据量场景
|
||||||
|
- 新增表必须在 migration 中同步创建必要索引,不得事后补建
|
||||||
|
- 高写入表(`follow_logs`、`property_photos`、`permission_change_logs`、`login_attempts`)必须按月分区(`PARTITION BY RANGE`)
|
||||||
|
|
||||||
|
### 4.6 安全约定
|
||||||
|
|
||||||
|
- 手机号等 PII 数据统一通过 `core/encryption.py` 加密存储,**禁止明文入库**
|
||||||
|
- 所有配置(密钥、Bucket 名、外部服务 URL)通过 `.env` 注入,**禁止硬编码**
|
||||||
|
- 每个受权限保护的 View 必须覆盖三个测试场景:有权限(200)、无权限(403)、未登录(302)
|
||||||
|
- Redis Key 必须携带租户 Schema 前缀,格式:`{tenant_schema}:{module}:{key}`
|
||||||
|
|
||||||
|
### 4.7 错误处理约定
|
||||||
|
|
||||||
|
- 后端 API 返回标准 JSON 错误格式:`{"error": "...", "code": "SNAKE_CASE_CODE"}`
|
||||||
|
- View 层禁止直接抛出异常,必须捕获并返回对应 HTTP 状态码
|
||||||
|
- Celery 任务失败必须记录到 Sentry,并更新任务状态表
|
||||||
|
|
||||||
|
### 4.8 文件命名约定
|
||||||
|
|
||||||
|
- Django App:`snake_case`(如 `property`、`follow_log`)
|
||||||
|
- 前端模板组件:`kebab-case`(如 `property-card.html`、`client-form.html`)
|
||||||
|
- Migration 文件:不得手动重命名,保留 Django 自动生成的序号
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 禁止项(Do NOT — 违反视为 Bug)
|
||||||
|
|
||||||
|
- ❌ **React / Vue / Angular** 等重前端框架
|
||||||
|
- ❌ 在请求线程中处理耗时 **> 500ms** 的任务(必须用 Celery)
|
||||||
|
- ❌ 传统页面全刷方案(除初始页面加载外)
|
||||||
|
- ❌ 复杂原生 JavaScript(优先 HTMX/Alpine 指令)
|
||||||
|
- ❌ Electron 渲染进程开启 `nodeIntegration: true`
|
||||||
|
- ❌ 客户端内嵌业务逻辑或本地数据库(壳应用原则,渲染层只加载 Web URL)
|
||||||
|
- ❌ 跨租户 SQL 查询(必须经 `django-tenants` 中间件切换 Schema)
|
||||||
|
- ❌ 代码中硬编码密钥、Tenant ID、URL、枚举中文字符串
|
||||||
|
- ❌ 使用 OFFSET 分页处理 1000 条以上数据集
|
||||||
|
- ❌ 手机号/身份证号明文存储
|
||||||
|
- ❌ 使用 Django 原生 `Client()` 做集成测试(必须用 `TenantClient`)
|
||||||
|
- ❌ 在 MVP 阶段实现 `PRD_MVP.md §3` 中标注的 Out-of-Scope 功能(移动端、合同、财务、新房、三网发布等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 测试要求
|
||||||
|
|
||||||
|
**核心原则**:每个 P0 User Story 完成后,对应测试必须同步产出,**不允许欠测试债**。
|
||||||
|
|
||||||
|
### 测试分层
|
||||||
|
|
||||||
|
| 层级 | 工具 | 覆盖目标 | 运行频率 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| **单元测试** | `pytest-django` + `factory_boy` | `core/`、`services/`、`tasks.py` | 每次 push |
|
||||||
|
| **集成测试** | `pytest-django` + `TenantClient` | 所有 P0 User Story 的 HTTP 接口 | 每次 push |
|
||||||
|
| **E2E 测试** | `playwright` (Python) | 5 条核心用户旅程 | 每日定时 |
|
||||||
|
|
||||||
|
### 测试关键约定
|
||||||
|
|
||||||
|
- 集成测试**必须**使用 `TenantClient`,禁止使用 Django 原生 `Client()`
|
||||||
|
- HTMX 局部请求测试须携带 `HTTP_HX_REQUEST: true` header,验证返回局部 HTML 而非完整页面
|
||||||
|
- Celery 任务测试使用 `CELERY_TASK_ALWAYS_EAGER = True` 同步执行
|
||||||
|
- 外部服务(R2、Redis、邮件)在测试中全部 Mock,禁止真实调用
|
||||||
|
- 权限测试必须覆盖:有权限(200)、无权限(403)、未登录(302)三场景
|
||||||
|
|
||||||
|
### 覆盖率基准
|
||||||
|
|
||||||
|
| 模块 | 最低目标 |
|
||||||
|
|------|---------|
|
||||||
|
| `core/` 核心基础模块 | ≥ 90% |
|
||||||
|
| `apps/*/services/` 业务逻辑层 | ≥ 80% |
|
||||||
|
| `apps/*/views.py` 视图层 | ≥ 70% |
|
||||||
|
| E2E 核心用户旅程(5 条) | 100% 通过 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. MVP 范围(Phase 1)
|
||||||
|
|
||||||
|
**当前阶段**:MVP Phase 1(P0 功能)
|
||||||
|
|
||||||
|
实现任何功能前,先对照 `PRD/PRD_MVP.md` 确认是否在 P0 范围。**P0 范围以外的功能在 MVP 阶段禁止实现**,包括但不限于:
|
||||||
|
|
||||||
|
- 移动端适配(v2 规划)
|
||||||
|
- 新房模块、合同、财务、三网发布
|
||||||
|
- 公客管理(P2)
|
||||||
|
- 门店分布地图(P2)
|
||||||
|
|
||||||
|
**当前 Phase 1 P0 Task 列表**:见 `PRD/TASK.md §Phase 1`(US-ACCOUNT-001~003、US-COMPLEX-001~003、US-PROPERTY-001~008、US-CLIENT-001~017、US-ORG-001~003、US-PERMISSION-001~005、US-SETTING-001)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 数据模型参考(实现前必读)
|
||||||
|
|
||||||
|
所有数据模型的**权威来源**如下,开发前必须阅读对应文档,不得凭印象实现:
|
||||||
|
|
||||||
|
| 模块 | 数据模型权威文档 |
|
||||||
|
|------|---------------|
|
||||||
|
| 总览 & 架构决策 | `DATA_MODEL/DATA_MODEL.md` |
|
||||||
|
| 房源管理 | `DATA_MODEL/DATA_MODEL_PROPERTY.md` |
|
||||||
|
| 客源管理 | `DATA_MODEL/DATA_MODEL_CLIENT.md` |
|
||||||
|
| 楼盘/小区/区域 | `DATA_MODEL/DATA_MODEL_COMPLEX.md` |
|
||||||
|
| 组织人事 | `DATA_MODEL/DATA_MODEL_ORG.md` |
|
||||||
|
| 权限管理 | `DATA_MODEL/DATA_MODEL_PERMISSION.md` |
|
||||||
|
| 登录认证 | `DATA_MODEL/DATA_MODEL_LOGIN.md` |
|
||||||
|
| Public Schema(平台运营层) | `DATA_MODEL/DATA_MODEL_PUBLIC.md` |
|
||||||
|
| 系统配置(lookup/setting) | `DATA_MODEL/DATA_MODEL_SETTING.md` |
|
||||||
|
|
||||||
|
### 核心领域关系速览
|
||||||
|
|
||||||
|
```
|
||||||
|
[区域/商圈] ──────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
[学校管理] │
|
||||||
|
│ ▼
|
||||||
|
[楼盘/小区] ── [楼栋] ──────────► [房源] ◄──── [挂牌历史]
|
||||||
|
│ │
|
||||||
|
│ ┌────────┼────────┐
|
||||||
|
│ │ │ │
|
||||||
|
│ [联系人] [跟进日志] [维护完成度]
|
||||||
|
│
|
||||||
|
[客源] ──── [配对记录] ──── [带看记录]
|
||||||
|
│
|
||||||
|
[员工/组织] ──── [权限]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 技术方案文档导航
|
||||||
|
|
||||||
|
| 模块 | 技术方案文档 | PRD |
|
||||||
|
|------|------------|-----|
|
||||||
|
| 总纲 & 禁止项 | `TECH_STACK/TECH_STACK.md` | `PRD/PRD_MVP.md` |
|
||||||
|
| 登录认证 | `TECH_STACK/登录管理技术方案.md` | `PRD/登录管理/` |
|
||||||
|
| 权限管理 | `TECH_STACK/权限管理系统技术方案.md` | `PRD/权限管理/` |
|
||||||
|
| 测试规范 | `TECH_STACK/测试规范.md` | — |
|
||||||
|
| 房源管理 | `TECH_STACK/房源管理技术方案.md` | `PRD/房源管理/` |
|
||||||
|
| 客源管理 | `TECH_STACK/客源管理技术方案.md` | `PRD/客源管理/` |
|
||||||
|
| 楼盘管理 | `TECH_STACK/楼盘管理技术方案.md` | `PRD/房源管理/(含楼盘)` |
|
||||||
|
| 组织人事 | `TECH_STACK/组织人事技术方案.md` | `PRD/组织人事管理/` |
|
||||||
|
| 系统设置 | `TECH_STACK/系统设置技术方案.md` | `PRD/系统配置/`、`PRD/系统管理/` |
|
||||||
|
| 客户端发布 | `TECH_STACK/客户端发布管理技术方案.md` | `PRD/发布管理/客户端发布管理模块PRD.md` |
|
||||||
|
| ADR 决策记录 | `ADR.md` | 全局(TECH/REQ) |
|
||||||
|
|
||||||
|
**设计 Review 记录**(了解当前已知技术债与阻塞项):
|
||||||
|
- `REVIEW/REVIEW_全局_2026-04-26.md`(最新)
|
||||||
|
- `REVIEW/REVIEW_全局_2026-04-25.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.1 ADR 治理联动规则
|
||||||
|
|
||||||
|
- 任何跨模块规则、口径或范围边界调整,先记录 `ADR.md`,再改对应 TECH_STACK/PRD/DATA_MODEL/TEST_CASES 文档。
|
||||||
|
- 若已有结论被替代,必须在 ADR 中新增 superseded 记录并引用新 ADR ID,禁止直接覆盖历史。
|
||||||
|
- 涉及上述变更的 PR/提交说明,必须包含 ADR ID(`ADR-YYYYMMDD-XXX`)。
|
||||||
|
|
||||||
|
## 10. 已知待解决问题(编码前必须确认)
|
||||||
|
|
||||||
|
以下问题在开始编码前需先确认当前状态,若未解决应在实现对应模块前暂停并上报:
|
||||||
|
|
||||||
|
| 编号 | 级别 | 问题描述 | 影响模块 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| **B-01** | 🔴 Blocker | 系统配置 PRD(`PRD/系统配置/系统配置.md`)为空骨架,US-SETTING-001 无法启动 | 系统配置 |
|
||||||
|
| **B-02** | 🔴 Blocker | 核心枚举三方不一致(PRD ↔ DDL ↔ TASK AC):客源 status/grade、房源 status 互相矛盾,需先冻结 `DATA_MODEL/ENUMS.md` | 房源、客源 |
|
||||||
|
| **B-03** | 🔴 Blocker | 权限数据范围档位冲突:§3 非目标"三档" vs §5.6"五档",DataScope 实现方式未统一 | 权限管理 |
|
||||||
|
| **B-04** | 🔴 Blocker | Keyset 分页规范完全缺位,89k 房源列表设计错误 | 房源、客源 |
|
||||||
|
| **M-02** | 🟠 Major | 主表 `properties`/`clients` 缺乏乐观锁字段 `version` | 房源、客源 |
|
||||||
|
| **M-03** | 🟠 Major | 高写入表分区 DDL 未落地(`follow_logs` 等) | 跟进日志 |
|
||||||
|
| **M-04** | 🟠 Major | Celery 多租户 schema 切换规范、R2 文件路径前缀规范、查询索引矩阵未补充 | 全局 |
|
||||||
|
|
||||||
|
> 详细内容见 `REVIEW/REVIEW_全局_2026-04-26.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 文档根目录
|
||||||
|
|
||||||
|
所有项目文档位于:`/mnt/d/Workspace/nexus/Project/fonrey/`
|
||||||
|
|
||||||
|
```
|
||||||
|
fonrey/
|
||||||
|
├── AGENTS.md # 本文件(AI Agent 开发指南)
|
||||||
|
├── PRD/
|
||||||
|
│ ├── PRD_MVP.md # MVP 范围书(必读)
|
||||||
|
│ ├── TASK.md # 全量 Task Board(US 编号)
|
||||||
|
│ ├── 房源管理/
|
||||||
|
│ ├── 客源管理/
|
||||||
|
│ ├── 楼盘管理/
|
||||||
|
│ ├── 组织人事管理/
|
||||||
|
│ ├── 权限管理/
|
||||||
|
│ ├── 登录管理/
|
||||||
|
│ ├── 系统配置/
|
||||||
|
│ ├── 系统管理/
|
||||||
|
│ └── 发布管理/
|
||||||
|
├── DATA_MODEL/
|
||||||
|
│ ├── DATA_MODEL.md # 数据模型总览(必读)
|
||||||
|
│ ├── DATA_MODEL_PROPERTY.md
|
||||||
|
│ ├── DATA_MODEL_CLIENT.md
|
||||||
|
│ ├── DATA_MODEL_COMPLEX.md
|
||||||
|
│ ├── DATA_MODEL_ORG.md
|
||||||
|
│ ├── DATA_MODEL_PERMISSION.md
|
||||||
|
│ ├── DATA_MODEL_LOGIN.md
|
||||||
|
│ └── DATA_MODEL_PUBLIC.md
|
||||||
|
├── TECH_STACK/
|
||||||
|
│ ├── TECH_STACK.md # 技术栈总纲(必读)
|
||||||
|
│ ├── 登录管理技术方案.md
|
||||||
|
│ ├── 权限管理系统技术方案.md
|
||||||
|
│ └── 测试规范.md
|
||||||
|
└── REVIEW/
|
||||||
|
├── REVIEW_全局_2026-04-26.md # 最新 Review(含阻塞问题)
|
||||||
|
└── REVIEW_全局_2026-04-25.md
|
||||||
|
```
|
||||||
854
Project/fonrey/DATA_MODEL/DATA_MODEL.md
Normal file
@@ -0,0 +1,854 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||||
|
# Fonrey 房产经纪管理系统 — DATA MODEL 设计文档
|
||||||
|
|
||||||
|
> **作者**: Backend Architect
|
||||||
|
> **版本**: v1.5
|
||||||
|
> **日期**: 2026-04-30(v1.1 修复 S1/S2/S4;v1.2 扩展 public schema;v1.3 §三 DDL 迁至 DATA_MODEL_PUBLIC.md,本文改为索引;v1.4 补充 LOGIN/PERMISSION 子文档引用、领域对象、租户 Schema 章节、Redis 缓存策略;v1.5 新增 ADR 导航与治理联动)
|
||||||
|
> **技术栈**: Django 4.x + PostgreSQL + django-tenants + Redis
|
||||||
|
> **设计目标**: 支撑 89,000+ 房源、多租户隔离、sub-100ms 查询、合规审计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 新增 ADR 导航与治理联动规则(数据模型变更需 ADR 可追溯) |
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、架构决策总览 (Architecture Decision Records)
|
||||||
|
|
||||||
|
### 1.1 多租户策略:Schema-per-Tenant
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PostgreSQL Instance │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────┐ ┌──────────────────┐ ┌────────────┐ │
|
||||||
|
│ │ public schema │ │ tenant_abc │ │ tenant_xyz │ │
|
||||||
|
│ │ (平台运营层) │ │ schema │ │ schema │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ - tenants │ │ - org_units │ │ (同左) │ │
|
||||||
|
│ │ - domains │ │ - staff │ │ │ │
|
||||||
|
│ │ - tenant_status_logs │ │ - complexes │ │ │ │
|
||||||
|
│ │ - platform_admins │ │ - properties │ │ │ │
|
||||||
|
│ │ - admin_mfa_devices │ │ - clients │ │ │ │
|
||||||
|
│ │ - admin_sessions │ │ - user_accounts │ │ │ │
|
||||||
|
│ │ - ip_whitelist │ │ - login_attempts │ │ │ │
|
||||||
|
│ │ - platform_audit_logs │ │ - permission_defs│ │ │ │
|
||||||
|
│ │ - backup_schedules │ │ - roles │ │ │ │
|
||||||
|
│ │ - backup_records │ │ - staff_roles │ │ │ │
|
||||||
|
│ │ - export_tasks │ │ - lookup_items │ │ │ │
|
||||||
|
│ │ - system_versions │ │ - ... │ │ │ │
|
||||||
|
│ │ - upgrade_events │ └──────────────────┘ └────────────┘ │
|
||||||
|
│ │ - enum_labels │ │
|
||||||
|
│ └─────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**选型理由**:
|
||||||
|
- `django-tenants` 的 Schema 隔离提供最强的数据安全边界
|
||||||
|
- 房产经纪公司之间数据绝对不能互通(合规要求)
|
||||||
|
- 每个 Schema 独立索引,避免全局锁竞争
|
||||||
|
- 支持按租户独立备份/恢复
|
||||||
|
|
||||||
|
### 1.2 核心领域模型关系图
|
||||||
|
|
||||||
|
```
|
||||||
|
[区域/商圈]──────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
[学校管理] │
|
||||||
|
│ ▼
|
||||||
|
[楼盘/小区] ──── [楼栋] ─────────► [房源] ◄──── [挂牌历史]
|
||||||
|
│ │
|
||||||
|
│ ┌────────┼────────┐
|
||||||
|
│ │ │ │
|
||||||
|
│ [联系人] [跟进日志] [维护完成度]
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────┘ ┌────┴──────┐
|
||||||
|
│ │ │ │
|
||||||
|
│ [电话查看] [钥匙] [委托] [实勘]
|
||||||
|
│
|
||||||
|
[客源] ──── [配对记录] ──── [带看记录]
|
||||||
|
│
|
||||||
|
[员工/组织] ──── [权限]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 关键设计原则
|
||||||
|
|
||||||
|
| 原则 | 决策 |
|
||||||
|
| ----- | -------------------------------------- |
|
||||||
|
| 主键类型 | `UUID v4`(跨环境安全,避免枚举攻击) |
|
||||||
|
| 软删除 | 所有核心表含 `deleted_at`(历史可追溯) |
|
||||||
|
| 时间戳 | 全部使用 `TIMESTAMPTZ`(含时区) |
|
||||||
|
| 手机号存储 | AES-256-GCM 加密存储,建立 SHA-256 哈希索引 |
|
||||||
|
| 审计字段 | `created_by`, `updated_by` 全表覆盖 |
|
||||||
|
| 枚举值 | 业务枚举用 `VARCHAR` + CHECK,系统枚举用 lookup 表 |
|
||||||
|
| 大文本 | `TEXT` 类型,不设长度(PG 内部优化) |
|
||||||
|
| 金额 | `NUMERIC(12,2)` 万元精度,避免浮点误差 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
本节用业务语言描述系统的核心领域对象及其关系,作为各子模块数据模型的导读。
|
||||||
|
|
||||||
|
### 核心领域对象
|
||||||
|
|
||||||
|
#### Public Schema(平台运营层)
|
||||||
|
|
||||||
|
| 领域对象 | 表 | 业务说明 |
|
||||||
|
|----------|-----|----------|
|
||||||
|
| **Tenant(租户)** | `public.tenants` | 每家房产经纪公司一条记录,含状态机(creating/active/suspended/pending_delete/deleted)、套餐、联系人 |
|
||||||
|
| **Domain(域名)** | `public.domains` | 子域名↔租户映射,支持多域名绑定,子域名创建后不可修改 |
|
||||||
|
| **TenantStatusLog** | `public.tenant_status_logs` | 租户状态变更不可变审计(append-only) |
|
||||||
|
| **PlatformAdmin** | `public.platform_admins` | 平台管理员账号,3 种角色:超级管理员/运营人员/只读审计员 |
|
||||||
|
| **AdminMfaDevice** | `public.admin_mfa_devices` | 管理员 TOTP 设备(强制启用) |
|
||||||
|
| **AdminSession** | `public.admin_sessions` | 登录会话(30 分钟超时,支持强制登出) |
|
||||||
|
| **IpWhitelist** | `public.ip_whitelist` | 管理控制台 CIDR 白名单 |
|
||||||
|
| **PlatformAuditLog** | `public.platform_audit_logs` | 所有写操作+高危操作审计(append-only,建议月度分区) |
|
||||||
|
| **BackupSchedule** | `public.backup_schedules` | 全局/租户级定时备份计划(频率/保留数/存储目标) |
|
||||||
|
| **BackupRecord** | `public.backup_records` | 备份任务执行记录(自动/手动/升级前/恢复前) |
|
||||||
|
| **ExportTask** | `public.export_tasks` | 数据导出异步任务(CSV/JSON/SQL Dump,24h 下载链接) |
|
||||||
|
| **SystemVersion** | `public.system_versions` | 平台版本历史,唯一 current 版本约束 |
|
||||||
|
| **UpgradeEvent** | `public.upgrade_events` | 升级/回滚事件,含灰度租户维度进度快照 |
|
||||||
|
| **EnumLabel** | `public.enum_labels` | 固定枚举字典(英文 Key → 中文标签),所有租户共享,供前端下拉渲染、导出报表中文标签、日志快照使用 |
|
||||||
|
|
||||||
|
#### Tenant Schema(租户业务层)
|
||||||
|
|
||||||
|
| 领域对象 | 表/子文档 | 业务说明 |
|
||||||
|
|----------|-----------|----------|
|
||||||
|
| **OrgUnit(组织架构)** | `org_units` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 树形组织架构(总部/区域/城市/大区/分公司/门店/团队/虚拟团队),物化路径存储,支持权限继承 |
|
||||||
|
| **Staff(员工)** | `staff` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 经纪人/店长/经理,绑定组织节点,手机号加密存储,与账号(登录)分离 |
|
||||||
|
| **District(城区)** | `districts` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 行政区划,如「静安区」,是区域体系的顶层节点 |
|
||||||
|
| **BusinessArea(商圈)** | `business_areas` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 商圈/板块,从属于城区,一个楼盘可归属多个商圈 |
|
||||||
|
| **School(学校)** | `schools` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 对口学校数据库,是买家购房决策的核心参考,与楼盘多对多关联 |
|
||||||
|
| **Complex(楼盘/小区)** | `complexes` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 房源录入的基础底座,维护楼盘标准名称/坐标/锁定状态/别名等 |
|
||||||
|
| **Building(楼栋/单元)** | `buildings` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘下的物理楼栋,区分标准结构与非标结构 |
|
||||||
|
| **RoomUnit(房号)** | `room_units` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼层+房间号,房源定位的最细粒度 |
|
||||||
|
| **Property(房源)** | `properties` → [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
|
||||||
|
| **Client(客源)** | `clients` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 买家/租客档案,分私客/公客/成交客,含活跃度评分与自动公客转换机制 |
|
||||||
|
| **Viewing(带看)** | `client_viewings` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 经纪人带客户看房的完整记录 |
|
||||||
|
| **Match(配对)** | `client_property_matches` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 系统/人工推荐的客源↔房源配对 |
|
||||||
|
| **UserAccount(用户账号)** | `user_accounts` → [DATA_MODEL_LOGIN.md](./DATA_MODEL_LOGIN.md) | 系统登录主体,与员工档案 1:1 绑定,含账号锁定/密码历史/登录审计 |
|
||||||
|
| **PermissionDef(权限定义)** | `permission_defs` → [DATA_MODEL_PERMISSION.md](./DATA_MODEL_PERMISSION.md) | 权限目录(约 300 条),驱动 Hybrid RBAC + Override 权限模型 |
|
||||||
|
| **Role(业务角色)** | `roles` → [DATA_MODEL_PERMISSION.md](./DATA_MODEL_PERMISSION.md) | 权限模板,含 4 大类别(置业顾问/店管/总经/运营/自定义) |
|
||||||
|
|
||||||
|
### 领域关系快速导航
|
||||||
|
|
||||||
|
```
|
||||||
|
District (城区)
|
||||||
|
└─ BusinessArea (商圈)
|
||||||
|
└─ Complex (楼盘) ─── School (对口学校)
|
||||||
|
├─ Building (楼栋)
|
||||||
|
│ └─ RoomUnit (房号)
|
||||||
|
└─ Property (房源)
|
||||||
|
├─ PropertyContact (联系人/委托方)
|
||||||
|
├─ FollowLog (跟进日志)
|
||||||
|
├─ Viewing (带看记录) ──── Client (客源)
|
||||||
|
└─ Match (配对记录) ──────┘
|
||||||
|
|
||||||
|
OrgUnit (组织架构)
|
||||||
|
└─ Staff (员工/经纪人) ─── Property / Client / Viewing / Match
|
||||||
|
```
|
||||||
|
|
||||||
|
### 子文档索引
|
||||||
|
|
||||||
|
| 子文档 | 覆盖模块 | 状态 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| [DATA_MODEL_PUBLIC.md](./DATA_MODEL_PUBLIC.md) | Public schema 平台运营层(tenants, domains, platform_admins, admin_sessions, audit_logs, backup, export, upgrade 共 13 张表) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 组织人事(org_units, staff, 异动/奖惩/教育/家庭等) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘/区域(districts, business_areas, complexes, buildings, room_units, schools 等) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 客源管理(clients, requirements, follow_logs, viewings, matches 等) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 房源管理(properties 及配套 22 张表,含跟进/钥匙/委托/实勘/营销/产证/完成度/标签/收藏/保护/号码方审批等) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_LOGIN.md](./DATA_MODEL_LOGIN.md) | 登录与账号认证(user_accounts, login_attempts, password_reset_tokens, password_histories + Redis 登录缓存) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_PERMISSION.md](./DATA_MODEL_PERMISSION.md) | 权限管理(permission_defs, roles, role_permissions, staff_roles, staff_permission_overrides, staff_data_scopes, permission_change_logs + Redis 权限缓存) | ✅ 完成 |
|
||||||
|
| [ENUMS.md](./ENUMS.md) | 枚举字典(`public.enum_labels` 表设计 + 所有模块枚举定义 + 种子数据 SQL) | ✅ 完成 |
|
||||||
|
| [ADR.md](../ADR.md) | 架构与需求决策记录(按日期/按模块/历史流水,append-only) | ✅ 完成 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、公共 Schema(Shared / Public)
|
||||||
|
|
||||||
|
> **权威源**:完整 DDL 已迁至 [`DATA_MODEL_PUBLIC.md`](./DATA_MODEL_PUBLIC.md),本节仅保留摘要索引。
|
||||||
|
> **覆盖范围**:`public` schema 存储平台运营层数据——租户注册、管理员账号、审计日志、备份/导出任务、版本升级记录(共 13 张表)。
|
||||||
|
> **设计依据**:系统管理模块 PRD(`PRD/系统管理/系统管理模块PRD.md`)。
|
||||||
|
|
||||||
|
### 表清单(开发以 DATA_MODEL_PUBLIC.md 为准)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 节 |
|
||||||
|
|------|------|----|
|
||||||
|
| `public.tenants` | 租户主表(django-tenants 核心,状态机 6 态) | §2.1 |
|
||||||
|
| `public.domains` | 域名↔租户映射(支持多域名,子域名不可修改) | §2.1 |
|
||||||
|
| `public.tenant_status_logs` | 租户状态变更不可变审计日志(append-only) | §2.1 |
|
||||||
|
| `public.platform_admins` | 平台管理员账号(super_admin/ops_operator/read_only_auditor) | §2.2 |
|
||||||
|
| `public.admin_mfa_devices` | 管理员 TOTP MFA 设备(强制启用) | §2.2 |
|
||||||
|
| `public.admin_sessions` | 管理员登录会话(30 min 滚动超时,支持强制登出) | §2.2 |
|
||||||
|
| `public.ip_whitelist` | 管理控制台 CIDR 白名单 | §2.2 |
|
||||||
|
| `public.platform_audit_logs` | 所有写操作+高危操作审计(append-only,建议月度分区) | §2.3 |
|
||||||
|
| `public.backup_schedules` | 全局/租户级定时备份计划(NULL tenant_id = 全局默认) | §2.4 |
|
||||||
|
| `public.backup_records` | 备份任务执行记录(auto/manual/pre_upgrade/pre_restore) | §2.4 |
|
||||||
|
| `public.export_tasks` | 数据导出异步任务(CSV/JSON/SQL Dump,24h 下载链接) | §2.4 |
|
||||||
|
| `public.system_versions` | 平台版本历史,部分唯一索引保证唯一 current | §2.5 |
|
||||||
|
| `public.upgrade_events` | 升级/回滚事件,`tenant_progress` JSONB 快照各租户状态 | §2.5 |
|
||||||
|
| `public.enum_labels` | 固定枚举字典(英文 Key → 中文标签),所有租户共享 | §2.6 |
|
||||||
|
|
||||||
|
**关键约束提示**:
|
||||||
|
- `tenant_status_logs` / `platform_audit_logs` **无 deleted_at**,禁止 UPDATE/DELETE,append-only
|
||||||
|
- `public.tenants.schema_name` 创建后**不可修改**
|
||||||
|
- `public.tenants` 不再使用 `is_active` boolean,改用 6 态 `status` 枚举
|
||||||
|
- `platform_admins` 与租户 `staff` **完全独立**,不共享 auth 系统
|
||||||
|
- `system_versions` 通过部分唯一索引确保全局只有一个 `status='current'`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- §三 DDL 已迁至 DATA_MODEL_PUBLIC.md v1.0(2026-04-24) -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 四、租户 Schema(Tenant Schema)
|
||||||
|
|
||||||
|
以下所有表均在每个租户的独立 Schema 内创建。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.1 组织人事模块(Organization & HR)
|
||||||
|
|
||||||
|
> **详细模型** → 见 [`DATA_MODEL_ORG.md`](./DATA_MODEL_ORG.md)
|
||||||
|
> 该文件为权威定义,包含完整字段、枚举、查询模式和禁止操作。
|
||||||
|
|
||||||
|
**核心表概览**(开发时以 DATA_MODEL_ORG.md 为准):
|
||||||
|
|
||||||
|
| 表名 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `org_units` | 组织树节点(公司/事业部/大区/区域/片区/门店/店组/职能),物化路径树 |
|
||||||
|
| `staff` | 员工主表,含加密手机号、角色、在职状态、Django auth 绑定 |
|
||||||
|
| `staff_personal_info` | 员工个人信息扩展(证件、学历、婚育等,1:1) |
|
||||||
|
| `staff_transfer_logs` | 人事异动不可变审计日志(入职/调动/离职/复职等) |
|
||||||
|
| `staff_reward_punish` | 奖惩记录 |
|
||||||
|
| `staff_work_experiences` | 工作经历 |
|
||||||
|
| `staff_educations` | 教育经历 |
|
||||||
|
| `staff_trainings` | 培训经历 |
|
||||||
|
| `staff_family_members` | 家庭成员 |
|
||||||
|
| `staff_accounts` | 第三方平台账号绑定(58安居客/中国网络经纪人等) |
|
||||||
|
|
||||||
|
**关键约束提示**:
|
||||||
|
- `staff.phone_enc` AES-256-GCM 加密,`staff.phone_hash` SHA-256 用于唯一索引
|
||||||
|
- `staff_transfer_logs` **无 deleted_at**,不可删除
|
||||||
|
- `org_units` 路径查询:`WHERE path LIKE '/root/{target_id}/%'`
|
||||||
|
- 员工离职:`status = 'resigned'` + `deleted_at` 软删除,记录永久保留
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 区域与楼盘模块(Region & Complex Management)
|
||||||
|
|
||||||
|
> **详细模型** → 见 [`DATA_MODEL_COMPLEX.md`](./DATA_MODEL_COMPLEX.md)
|
||||||
|
> 本节仅作概览,开发时以 DATA_MODEL_COMPLEX.md 为权威定义。
|
||||||
|
|
||||||
|
**核心表概览**(开发时以 DATA_MODEL_COMPLEX.md 为准):
|
||||||
|
|
||||||
|
| 表名 | 说明 | 关键字段 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `districts` | 城区/行政区 | `city`, `name`, `short_name`, `sort_order` |
|
||||||
|
| `business_areas` | 商圈/板块(从属于城区) | `district_id`, `name`, `latitude`, `longitude` |
|
||||||
|
| `metro_lines` | 地铁线路 | `city`, `name`, `color` |
|
||||||
|
| `metro_stations` | 地铁站点 | `metro_line_id`, `name`, `latitude`, `longitude` |
|
||||||
|
| `schools` | 学校(对口学区) | `district_id`, `name`, `type`, `nature`, `level` |
|
||||||
|
| `complexes` | 楼盘/小区(房源底座) | `name`, `district_id`, `address`, `latitude/longitude`, `lock_*`, `search_vector` |
|
||||||
|
| `complex_aliases` | 楼盘别名(含系统别名/用户自定义别名) | `complex_id`, `alias`, `is_system` |
|
||||||
|
| `complex_business_areas` | 楼盘↔商圈多对多(含主商圈标识) | `complex_id`, `business_area_id`, `is_primary` |
|
||||||
|
| `complex_schools` | 楼盘↔学校关联(含学区类型) | `complex_id`, `school_id`, `zone_type` |
|
||||||
|
| `complex_metro_stations` | 楼盘↔地铁站关联(含步行距离) | `complex_id`, `station_id`, `distance_meters` |
|
||||||
|
| `buildings` | 楼栋/单元 | `complex_id`, `name`, `is_standard`, `total_floors` |
|
||||||
|
| `room_units` | 房号/结构单元(楼层+房间号) | `building_id`, `floor`, `room_no`, `is_standard` |
|
||||||
|
| `complex_photos` | 楼盘照片(楼盘图/户型图/VR) | `complex_id`, `category`, `file_key`, `is_cover` |
|
||||||
|
| `complex_attachments` | 楼盘附件 | `complex_id`, `file_key`, `file_name` |
|
||||||
|
| `complex_price_trends` | 楼盘价格走势(月度) | `complex_id`, `record_month`, `avg_unit_price` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 房源模块(Property Management)
|
||||||
|
|
||||||
|
> **详细模型** → 见 [`DATA_MODEL_PROPERTY.md`](./DATA_MODEL_PROPERTY.md)
|
||||||
|
> 本节仅作概览,开发时以 DATA_MODEL_PROPERTY.md 为权威定义。
|
||||||
|
|
||||||
|
**核心表概览**(开发时以 DATA_MODEL_PROPERTY.md 为准):
|
||||||
|
|
||||||
|
| 表名 | 说明 | 关键字段 |
|
||||||
|
| ------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `properties` | 房源主表(系统核心,89,000+ 数据量) | `status`, `attribute`, `property_type`, `complex_id`, `sale_price`, `area`, `grade`, `completeness_score`, `search_vector` |
|
||||||
|
| `property_contacts` | 业主/联系人(手机号 AES 加密+哈希索引) | `property_id`, `phone_enc`, `phone_hash`, `identity`, `is_number_holder` |
|
||||||
|
| `listing_histories` | 挂牌历史快照(不可删除) | `property_id`, `listing_type`, `status`, `sale_price`, `seller_agent_snapshot` |
|
||||||
|
| `price_changes` | 调价记录(不可删除) | `property_id`, `old_sale_price`, `new_sale_price`, `change_reason`, `changed_by` |
|
||||||
|
| `follow_logs` | 跟进日志(6种类型,最高写入频率) | `property_id`, `log_type`, `content`, `is_deletable`, `operator_id` |
|
||||||
|
| `follow_log_attachments` | 跟进附件(图片) | `follow_log_id`, `file_key`, `file_type` |
|
||||||
|
| `follow_log_recordings` | 跟进录音 | `follow_log_id`, `file_key`, `duration_seconds` |
|
||||||
|
| `property_keys` | 钥匙管理(机械钥匙/密码) | `property_id`, `key_type`, `holder_id`, `is_active` |
|
||||||
|
| `key_attachments` | 钥匙附件 | `key_id`, `file_key` |
|
||||||
|
| `commissions` | 委托管理(独家/非独家) | `property_id`, `commission_type`, `period_start`, `status` |
|
||||||
|
| `commission_attachments` | 委托附件(身份证/产证/委托书) | `commission_id`, `category`, `file_key` |
|
||||||
|
| `field_surveys` | 实勘管理(GPS 打卡) | `property_id`, `status`, `gps_latitude`, `gps_longitude`, `created_by` |
|
||||||
|
| `survey_photos` | 实勘照片(按空间分类) | `survey_id`, `category`, `file_key`, `is_vr_screenshot` |
|
||||||
|
| `property_photos` | 房源图片(经纪人管理,封面唯一约束) | `property_id`, `category`, `is_cover`, `file_key` |
|
||||||
|
| `property_attachments` | 房源附件 | `property_id`, `category`, `file_key` |
|
||||||
|
| `property_marketing` | 营销信息(1:1,卖点/业主心态/介绍) | `property_id`, `marketing_title`, `core_selling_points` |
|
||||||
|
| `property_certificates` | 产证信息(1:1) | `property_id`, `cert_no`, `owner_name`, `land_nature` |
|
||||||
|
| `property_completeness` | 维护完成度快照(1:1,Celery 异步计算) | `property_id`, `total_score`, `score_survey`, `score_commission`, ... |
|
||||||
|
| `property_tags` | 标签字典(系统预置+运营自定义) | `name`, `color`, `is_system` |
|
||||||
|
| `property_tag_relations` | 房源↔标签多对多 | `property_id`, `tag_id` |
|
||||||
|
| `property_favorites` | 经纪人收藏房源 | `staff_id`, `property_id` |
|
||||||
|
| `property_protections` | 保护房设置(1:1) | `property_id`, `is_protected`, `start_at`, `end_at` |
|
||||||
|
| `number_holder_approvals` | 号码方变更审批 | `property_id`, `applicant_id`, `status` |
|
||||||
|
|
||||||
|
**关键约束提示**:
|
||||||
|
- `property_contacts.phone_hash` 是重复房源检测的主要依据,录入前必须查重
|
||||||
|
- `listing_histories` / `price_changes` **无 deleted_at**,不可删除
|
||||||
|
- `follow_logs` 中 `is_deletable=FALSE`(`sensitive_view` 类型)不可软删
|
||||||
|
- `completeness_score` 只由 Celery 任务写入,Application 层禁止直接更新
|
||||||
|
- `last_followed_at` 由触发器 `trg_update_last_followed` 自动维护
|
||||||
|
- `property_photos.is_cover` 唯一约束:每套房源仅一张封面
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 登录与账号认证(Login & Account)
|
||||||
|
|
||||||
|
> **详细模型** → 见 [`DATA_MODEL_LOGIN.md`](./DATA_MODEL_LOGIN.md)
|
||||||
|
> 该文件为权威定义,包含完整字段、状态机、Redis 缓存结构和禁止操作。
|
||||||
|
|
||||||
|
**核心表概览**(开发时以 DATA_MODEL_LOGIN.md 为准):
|
||||||
|
|
||||||
|
| 表名 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `user_accounts` | 账号主表(1:1 绑定 `org.Staff`),含加密手机号/哈希、状态机(active/locked/disabled)、初始密码标识 |
|
||||||
|
| `login_attempts` | 登录审计日志(append-only,成功/失败均记录,无 FK 冗余存 username 保证历史完整) |
|
||||||
|
| `password_reset_tokens` | 密码重置 Token(有效期 30 分钟,使用后立即标记 `is_used`) |
|
||||||
|
| `password_histories` | 历史密码记录(保留最近 3 条,含初始密码,防止重复使用) |
|
||||||
|
|
||||||
|
**关键约束提示**:
|
||||||
|
- `user_accounts` 主键用 `BIGSERIAL`(非 UUID),登录审计场景 BigInt 更高效
|
||||||
|
- `user_accounts.phone_enc` AES-256-GCM 加密,`phone_hash` SHA-256 用于唯一索引
|
||||||
|
- **禁止物理删除** `user_accounts`,离职员工只能 `status=disabled`
|
||||||
|
- 账号锁定(5 次密码错误)→ `status=locked`,`locked_until=NOW()+30min`;Redis 仅计数,实际锁定以 DB 为准
|
||||||
|
- Tenant Admin 的 `staff_id` 可为空(可无员工档案);普通员工 `staff_id` 必填且关联 active Staff
|
||||||
|
- 员工离职(`org.Staff.status→resigned`)→ 应用层 Service 调用触发账号 `status→disabled`,**禁止循环 FK**
|
||||||
|
- `password_reset_tokens` / `login_attempts` **无 deleted_at**,不可修改/删除
|
||||||
|
|
||||||
|
**Redis 辅助状态**(非持久化):
|
||||||
|
|
||||||
|
| Key 格式 | TTL | 说明 |
|
||||||
|
|----------|-----|------|
|
||||||
|
| `captcha_token:{uuid}` | 3 分钟 | 滑块验证会话 Token |
|
||||||
|
| `captcha_pass:{uuid}` | 3 分钟 | 一次性通过凭证(验证后立即删除) |
|
||||||
|
| `login_fail:{tenant_id}:{username}` | 30 分钟 | 连续密码错误计数;≥5 触发锁定 |
|
||||||
|
| `recover_email:{email}` | 1 小时 | 找回邮件发送次数上限 3 次 |
|
||||||
|
| `tenant_verify_ip:{ip}` | 1 分钟 | Tenant 验证接口 IP 限流;≥10 次拒绝 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 权限管理(Permission & RBAC)
|
||||||
|
|
||||||
|
> **详细模型** → 见 [`DATA_MODEL_PERMISSION.md`](./DATA_MODEL_PERMISSION.md)
|
||||||
|
> 该文件为权威定义,包含完整字段、权限解析算法、`ScopeQueryBuilder` 实现和禁止操作。
|
||||||
|
|
||||||
|
**权限模型概述**:Hybrid RBAC + Individual Override,支持 `BOOLEAN / SCOPE / INTEGER` 三类权限值,多角色合并规则 OR / MAX。
|
||||||
|
|
||||||
|
**核心表概览**(开发时以 DATA_MODEL_PERMISSION.md 为准):
|
||||||
|
|
||||||
|
| 表名 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `permission_defs` | 权限目录(约 300 条,`PUBLIC Schema` 中 `shared_apps` 存储,所有租户共享),含模块/分组/值类型/默认值/上限类别 |
|
||||||
|
| `roles` | 业务角色(每租户独立),5 种类别:`agent/store_manager/director/operator/custom`,含系统内置标识 |
|
||||||
|
| `role_permissions` | 角色↔权限值(稀疏存储,仅存与 default_value 不同的项) |
|
||||||
|
| `staff_roles` | 员工↔角色分配(N:M,含主角色标识 `is_primary`、有效期) |
|
||||||
|
| `staff_permission_overrides` | 员工个人权限覆盖(稀疏存储,仅存与角色合并值不同的项),3 种 override_mode:REPLACE / RESTRICT / GRANT |
|
||||||
|
| `staff_data_scopes` | 员工数据范围扩展(补充 SCOPE 权限之外的额外可读范围,如特殊跨门店授权) |
|
||||||
|
| `permission_change_logs` | 权限变更不可变审计日志(append-only,禁止 UPDATE/DELETE) |
|
||||||
|
|
||||||
|
**关键约束提示**:
|
||||||
|
- `permission_defs` 位于 **Public Schema**(`shared_apps`),所有租户共享;`roles` 及其余表属租户 Schema
|
||||||
|
- **禁止硬删除** `permission_defs`,改用 `is_active=FALSE` 下线;`code` 字段不可修改
|
||||||
|
- **禁止直接构造 Q 对象绕过 `ScopeQueryBuilder`**,会导致越权漏洞
|
||||||
|
- `permission_change_logs` **无 deleted_at**,禁止 UPDATE/DELETE
|
||||||
|
- 员工权限解析:`is_system_admin=TRUE` → 短路返回全权限;否则多角色 OR/MAX 合并后叠加 Override
|
||||||
|
- `StaffPermissionOverride` 保存前必须做差异对比,**禁止存与角色合并值相同的冗余记录**(稀疏存储)
|
||||||
|
- `staff_roles.is_primary` 唯一约束通过 Signal 维护,**禁止绕过**
|
||||||
|
|
||||||
|
**权限解析缓存**:
|
||||||
|
|
||||||
|
| Cache Key | TTL | 失效触发 |
|
||||||
|
|-----------|-----|---------|
|
||||||
|
| `perm:v{VER}:{schema}:{staff_id}` | 3600s | Override / StaffRole 变更 |
|
||||||
|
| `perm:v{VER}:{schema}:role:{role_id}:staff_ids` | 3600s | 角色权限变更 → Pipeline 批量失效 |
|
||||||
|
| `perm:inconsistent:{schema}:{staff_id}` | 300s | 同上 |
|
||||||
|
| `perm:defs:{schema}` | 86400s | PermissionDef 变更(低频) |
|
||||||
|
| `perm:role_applied_count:{schema}:{role_id}` | 600s | StaffRole 变更 |
|
||||||
|
|
||||||
|
> **版本号机制**:`CACHE_VERSION` 在 Django settings 中,升级 PermissionDef 结构时 bump,一键全局失效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.17 客源管理(Client Management)
|
||||||
|
|
||||||
|
> **详细模型** → 见 [`DATA_MODEL_CLIENT.md`](./DATA_MODEL_CLIENT.md)
|
||||||
|
> 该文件为权威定义,包含完整字段、枚举、状态机、查询模式和禁止操作。
|
||||||
|
|
||||||
|
**核心表概览**(开发时以 DATA_MODEL_CLIENT.md 为准):
|
||||||
|
|
||||||
|
| 表名 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `clients` | 客源主表(私客/公客/成交客),含加密手机号哈希、活跃度、归属人 |
|
||||||
|
| `client_contacts` | 联系人(1:N),手机号加密+哈希,支持多联系人 |
|
||||||
|
| `client_requirements` | 需求信息(可多类型:二手/新房/租房),含预算/面积/商圈/朝向等偏好 |
|
||||||
|
| `client_follow_logs` | 跟进日志(高写入频率,5种类型,敏感查看类型不可删) |
|
||||||
|
| `client_follow_log_attachments` | 跟进附件(图片/录音,最大20MB) |
|
||||||
|
| `client_viewings` | 带看/预约记录(1:N,含陪看人/合作带看人) |
|
||||||
|
| `client_property_matches` | 智能配房结果(录客配房/系统配房,匹配度评分) |
|
||||||
|
| `client_status_logs` | 状态变更不可变审计日志(改状态/改等级/转公/转成交/转无效等) |
|
||||||
|
| `client_favorite_folders` | 私客收藏夹(经纪人自定义分组) |
|
||||||
|
| `client_folder_items` | 收藏夹与客源的多对多关联 |
|
||||||
|
| `client_school_preferences` | 意向学校(拆表,支持精确查询) |
|
||||||
|
|
||||||
|
**关键约束提示**:
|
||||||
|
- `client_contacts.phone_hash` 是重复客源检测的唯一依据,录入前必须查重
|
||||||
|
- `client_status_logs` **无 deleted_at**,不可删除
|
||||||
|
- 私客超时(配置天数内无跟进)→ Celery 自动转公(`transfer_to_public_type = 'auto'`)
|
||||||
|
- 活跃度 `activity_level` 由 Celery 每日凌晨批量计算,不实时更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.18 系统设置(System Settings)
|
||||||
|
|
||||||
|
> **归属说明**:
|
||||||
|
> - `lookup_categories` / `lookup_items` / `saved_filters` 为**跨模块**系统表,权威定义在本节。
|
||||||
|
> - `property_tags` / `property_tag_relations` / `property_favorites` / `property_protections` / `number_holder_approvals` 属房源模块配套表,**权威定义已迁至** [`DATA_MODEL_PROPERTY.md §4.19-§4.22`](./DATA_MODEL_PROPERTY.md),本节不再重复 DDL(修复 S1/S2)。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ============================================================
|
||||||
|
-- 枚举/选项管理:跟进目的、标签、来源渠道 等运营维护的枚举值
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE lookup_categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
code VARCHAR(50) UNIQUE NOT NULL, -- 如:follow_purpose, property_source
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
module VARCHAR(30) NOT NULL -- property/client/system
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE lookup_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
category_id UUID NOT NULL REFERENCES lookup_categories(id) ON DELETE CASCADE,
|
||||||
|
value VARCHAR(100) NOT NULL,
|
||||||
|
label VARCHAR(100) NOT NULL, -- 显示文本
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}', -- 扩展属性
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lookup_items_category ON lookup_items(category_id)
|
||||||
|
WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_lookup_items_value ON lookup_items(category_id, value);
|
||||||
|
|
||||||
|
-- 筛选方案(保存的搜索条件,跨模块通用)
|
||||||
|
CREATE TABLE saved_filters (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
module VARCHAR(20) NOT NULL DEFAULT 'property',
|
||||||
|
filter_params JSONB NOT NULL, -- 完整筛选参数 JSON
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_saved_filters_staff ON saved_filters(staff_id, module);
|
||||||
|
```
|
||||||
|
|
||||||
|
**已迁出本节的表**(权威源见子文档):
|
||||||
|
|
||||||
|
| 表名 | 权威定义位置 |
|
||||||
|
|------|-------------|
|
||||||
|
| `property_tags` | [`DATA_MODEL_PROPERTY.md §4.19`](./DATA_MODEL_PROPERTY.md) |
|
||||||
|
| `property_tag_relations` | [`DATA_MODEL_PROPERTY.md §4.19`](./DATA_MODEL_PROPERTY.md) |
|
||||||
|
| `property_favorites` | [`DATA_MODEL_PROPERTY.md §4.20`](./DATA_MODEL_PROPERTY.md) |
|
||||||
|
| `property_protections` | [`DATA_MODEL_PROPERTY.md §4.21`](./DATA_MODEL_PROPERTY.md) |
|
||||||
|
| `number_holder_approvals` | [`DATA_MODEL_PROPERTY.md §4.22`](./DATA_MODEL_PROPERTY.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.19 枚举字典(Enum Labels)
|
||||||
|
|
||||||
|
> **权威定义** → 见 [`DATA_MODEL/ENUMS.md`](./ENUMS.md)
|
||||||
|
> 本节为概览,开发时以 ENUMS.md 为准。
|
||||||
|
|
||||||
|
#### 表归属
|
||||||
|
|
||||||
|
`enum_labels` 位于 **Public Schema**(`shared_apps`),所有租户共享,**不属于任何租户 Schema**。
|
||||||
|
|
||||||
|
#### 核心表设计
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE enum_labels (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
domain VARCHAR(60) NOT NULL, -- 枚举域,格式:{模块}.{字段},如 client.status
|
||||||
|
value VARCHAR(60) NOT NULL, -- 英文 Key(与数据库 CHECK 约束一致)
|
||||||
|
label_zh VARCHAR(60) NOT NULL, -- 中文标签(前端展示用)
|
||||||
|
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
remark TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_enum_labels_domain_value ON enum_labels(domain, value);
|
||||||
|
CREATE INDEX idx_enum_labels_domain ON enum_labels(domain, sort_order);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 覆盖的枚举域(domain 清单)
|
||||||
|
|
||||||
|
| domain | 说明 | 对应表字段 |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `client.status` | 客源状态(8 态) | `clients.status` |
|
||||||
|
| `client.grade` | 客源等级(5 档 + E) | `clients.grade` |
|
||||||
|
| `client.requirement_type` | 需求类型(旧:`client.purpose_type`) | `client_requirements.requirement_type` |
|
||||||
|
| `client.property_usage` | 房源用途偏好(旧:`client.usage`) | `client_requirements.property_usage` |
|
||||||
|
| `client.orientation` | 朝向偏好 | `client_requirements.orientation` |
|
||||||
|
| `client.payment_method` | 付款方式 | `clients.payment_method` |
|
||||||
|
| `property.status` | 房源状态 | `properties.status` |
|
||||||
|
| `property.attribute` | 房源属性(公/私/保护) | `properties.attribute` |
|
||||||
|
| `property.property_type` | 房源类型(旧:`property.usage`) | `properties.property_type` |
|
||||||
|
| `property.grade` | 房源等级(5 档) | `properties.grade` |
|
||||||
|
| `property.listing_history.listing_type` | 挂牌类型(旧:`property.listing_type`) | `listing_histories.listing_type` |
|
||||||
|
| `property.decoration` | 装修程度 | `properties.decoration` |
|
||||||
|
| `property.orientation` | 朝向 | `properties.orientation` |
|
||||||
|
| `property.commission.status` | 委托状态(旧:`commission.type`) | `commissions.status` |
|
||||||
|
| `field_survey.status` | 实勘状态 | `field_surveys.status` |
|
||||||
|
| `follow_log.log_type` | 跟进日志类型 | `follow_logs.log_type` |
|
||||||
|
|
||||||
|
#### 重要约定
|
||||||
|
|
||||||
|
- `enum_labels.value` 必须与对应表的 `CHECK` 约束完全一致,**两者必须同步修改**
|
||||||
|
- 新增枚举值流程:① 修改 DDL `CHECK` 约束 → ② 插入 `enum_labels` 种子数据 → ③ 更新 `ENUMS.md`
|
||||||
|
- `is_active = FALSE` 仅停用前端展示,**不得修改或删除已有 `value`**(历史数据引用不可破坏)
|
||||||
|
- 前端下拉渲染**统一从 `enum_labels` 读取**,禁止在前端代码中硬编码中文标签
|
||||||
|
|
||||||
|
#### 与 `lookup_items` 的区别
|
||||||
|
|
||||||
|
| 对比维度 | `enum_labels` | `lookup_items` |
|
||||||
|
|---------|---------------|----------------|
|
||||||
|
| 用途 | 固定枚举的中文标签映射 | 运营可配置的动态选项(如跟进目的、来源渠道) |
|
||||||
|
| 修改权限 | 仅开发/DBA | 运营人员后台配置 |
|
||||||
|
| Schema 位置 | Public Schema(共享) | Tenant Schema(每租户独立) |
|
||||||
|
| 典型示例 | 客源状态、房源等级 | 跟进目的、客户来源渠道 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、关键索引汇总与查询优化策略
|
||||||
|
|
||||||
|
### 4.1 房源列表页核心查询分析
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 典型查询:出售状态 + 公盘 + 特定区域 + 价格区间 + 户型筛选 + 按挂牌日期排序
|
||||||
|
-- 优化方案:复合索引覆盖最高频维度组合
|
||||||
|
|
||||||
|
-- 高频组合索引(status + attribute,覆盖 90% 的列表查询)
|
||||||
|
CREATE INDEX idx_properties_list_composite ON properties
|
||||||
|
(status, attribute, complex_id, sale_price DESC NULLS LAST)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 与我相关查询(经纪人个人仪表板)
|
||||||
|
CREATE INDEX idx_properties_my_properties ON properties
|
||||||
|
(seller_agent_id, status, listed_at DESC NULLS LAST)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 全文搜索触发器(自动维护 search_vector)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 房源全文检索向量更新触发器
|
||||||
|
CREATE OR REPLACE FUNCTION update_property_search_vector()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.block_no, '') ||
|
||||||
|
' ' || COALESCE(NEW.unit_no, '') ||
|
||||||
|
' ' || COALESCE(NEW.room_no, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.remarks, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_property_search_vector
|
||||||
|
BEFORE INSERT OR UPDATE OF block_no, unit_no, room_no, remarks
|
||||||
|
ON properties
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_property_search_vector();
|
||||||
|
|
||||||
|
-- 楼盘全文检索向量(含别名,提升模糊搜索精度)
|
||||||
|
CREATE OR REPLACE FUNCTION update_complex_search_vector()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.alias, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_complex_search_vector
|
||||||
|
BEFORE INSERT OR UPDATE OF name, alias, address
|
||||||
|
ON complexes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 last_followed_at 自动维护触发器
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 每次写入跟进日志时,自动更新 properties.last_followed_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_property_last_followed()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.log_type = 'written' THEN
|
||||||
|
UPDATE properties
|
||||||
|
SET last_followed_at = NEW.created_at,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = NEW.property_id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_update_last_followed
|
||||||
|
AFTER INSERT ON follow_logs
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_property_last_followed();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Redis 缓存策略
|
||||||
|
|
||||||
|
### 5.1 缓存 Key 规范
|
||||||
|
|
||||||
|
```
|
||||||
|
# 格式:{tenant_schema}:{module}:{entity}:{id}:{field}
|
||||||
|
# TTL 单位:秒
|
||||||
|
|
||||||
|
# 房源详情(高频读取)
|
||||||
|
{schema}:prop:detail:{property_id} TTL: 300 (5分钟)
|
||||||
|
|
||||||
|
# 房源联系人(含解密号码,敏感,TTL 短)
|
||||||
|
{schema}:prop:contacts:{property_id} TTL: 60 (1分钟)
|
||||||
|
|
||||||
|
# 楼盘基础信息(低变更频率)
|
||||||
|
{schema}:complex:base:{complex_id} TTL: 3600 (1小时)
|
||||||
|
|
||||||
|
# 楼盘名称自动补全候选列表(联想搜索)
|
||||||
|
{schema}:complex:autocomplete:{prefix} TTL: 600 (10分钟)
|
||||||
|
|
||||||
|
# 员工信息(用于日志快照)
|
||||||
|
{schema}:staff:base:{staff_id} TTL: 1800 (30分钟)
|
||||||
|
|
||||||
|
# 枚举值/lookup(几乎不变)
|
||||||
|
{schema}:lookup:{category_code} TTL: 86400 (24小时)
|
||||||
|
|
||||||
|
# 登录模块(详见 DATA_MODEL_LOGIN.md §四)
|
||||||
|
captcha_token:{uuid} TTL: 180 (3分钟)
|
||||||
|
captcha_pass:{uuid} TTL: 180 (3分钟)
|
||||||
|
login_fail:{tenant_id}:{username} TTL: 1800 (30分钟,连续失败计数)
|
||||||
|
recover_email:{email} TTL: 3600 (1小时,发送次数限流)
|
||||||
|
recover_reset:{account_id} TTL: 3600 (1小时,Token 生成次数限流)
|
||||||
|
tenant_verify_ip:{ip} TTL: 60 (1分钟,IP 限流)
|
||||||
|
|
||||||
|
# 权限模块(详见 DATA_MODEL_PERMISSION.md §六)
|
||||||
|
perm:v{VER}:{schema}:{staff_id} TTL: 3600 (员工完整权限快照)
|
||||||
|
perm:v{VER}:{schema}:role:{role_id}:staff_ids TTL: 3600 (角色→员工 ID 列表,批量失效用)
|
||||||
|
perm:inconsistent:{schema}:{staff_id} TTL: 300 (权限不一致标记)
|
||||||
|
perm:defs:{schema} TTL: 86400 (权限定义全量缓存)
|
||||||
|
perm:role_applied_count:{schema}:{role_id} TTL: 600 (角色应用人数)
|
||||||
|
|
||||||
|
# 标签列表
|
||||||
|
{schema}:tags:property TTL: 3600
|
||||||
|
|
||||||
|
# 维护完成度(Celery 计算后写入,详情页直接读 Redis)
|
||||||
|
{schema}:prop:completeness:{property_id} TTL: 600
|
||||||
|
|
||||||
|
# 房源列表计数(筛选后总条数,避免 COUNT(*) 全扫)
|
||||||
|
{schema}:prop:count:{filter_hash} TTL: 30 (短TTL,保证准确性)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 缓存失效策略
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Django Signal 驱动的缓存失效(在 models.py 中注册)
|
||||||
|
|
||||||
|
# 房源更新 → 失效详情缓存 + 完成度缓存
|
||||||
|
# 跟进日志新增 → 失效 last_followed_at 缓存
|
||||||
|
# 联系人更新 → 失效联系人缓存(立即)
|
||||||
|
# 楼盘更新 → 失效楼盘缓存 + 相关房源缓存(批量)
|
||||||
|
# 枚举更新 → 失效对应 lookup 缓存
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、Django Model 层设计要点
|
||||||
|
|
||||||
|
### 6.1 抽象基类
|
||||||
|
|
||||||
|
```python
|
||||||
|
# models/base.py
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class UUIDPrimaryKeyModel(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class TimeStampedModel(UUIDPrimaryKeyModel):
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, db_index=False)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class SoftDeleteModel(TimeStampedModel):
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True, db_index=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def soft_delete(self, deleted_by=None):
|
||||||
|
from django.utils import timezone
|
||||||
|
self.deleted_at = timezone.now()
|
||||||
|
self.save(update_fields=['deleted_at', 'updated_at'])
|
||||||
|
|
||||||
|
class AuditedModel(SoftDeleteModel):
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
'staff.Staff', null=True, on_delete=models.SET_NULL,
|
||||||
|
related_name='+', db_column='created_by'
|
||||||
|
)
|
||||||
|
updated_by = models.ForeignKey(
|
||||||
|
'staff.Staff', null=True, on_delete=models.SET_NULL,
|
||||||
|
related_name='+', db_column='updated_by'
|
||||||
|
)
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 加密字段 Mixin
|
||||||
|
|
||||||
|
```python
|
||||||
|
# utils/encryption.py
|
||||||
|
# 手机号加密:AES-256-GCM + SHA-256 哈希索引
|
||||||
|
|
||||||
|
class EncryptedPhoneField:
|
||||||
|
"""
|
||||||
|
存储时:phone → AES加密 → phone_enc (BYTEA)
|
||||||
|
phone → SHA256 → phone_hash (VARCHAR 64)
|
||||||
|
查询时:phone_hash 走索引,phone_enc 解密展示
|
||||||
|
打码展示:前3位明文 + ******* + 后3位
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Manager 过滤软删除
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ActiveManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(deleted_at__isnull=True)
|
||||||
|
|
||||||
|
class PropertyManager(ActiveManager):
|
||||||
|
def public(self):
|
||||||
|
return self.get_queryset().filter(attribute='public')
|
||||||
|
|
||||||
|
def mine(self, staff_id):
|
||||||
|
return self.get_queryset().filter(seller_agent_id=staff_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、数据量与性能预测
|
||||||
|
|
||||||
|
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|
||||||
|
|------|---------|---------|---------|
|
||||||
|
| `properties` | 89,000+ | 中速 | 暂不分区,建议 500k 后按 `created_at` RANGE 分区 |
|
||||||
|
| `follow_logs` | 200万+ | 高速(最高频写入) | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
|
||||||
|
| `property_photos` | 500万+ | 高速 | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
|
||||||
|
| `permission_change_logs` | 100万+ | 中高速 | ✅ `PARTITION BY RANGE (operated_at)` 月度分区 |
|
||||||
|
| `login_attempts` | 500万+ | 高速(每次登录一条) | ✅ `PARTITION BY RANGE (attempted_at)` 月度分区 |
|
||||||
|
| `platform_audit_logs` | 10万+ | 低中速 | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
|
||||||
|
| `price_changes` | 50万 | 中速 | 无需分区 |
|
||||||
|
| `listing_histories` | 20万 | 低速 | 无需分区 |
|
||||||
|
| `clients` | 10万+ | 中速 | 暂不分区 |
|
||||||
|
| `viewings` | 100万 | 中速 | 无需分区 |
|
||||||
|
|
||||||
|
### 8.1 分区维护策略(partition_maintenance_task)
|
||||||
|
|
||||||
|
所有月度分区表统一由 **Celery Beat 定时任务** `partition_maintenance_task` 维护,每月 1 日凌晨 01:00(UTC+8)自动执行:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# apps/property/tasks.py(及 permission/login/shared 各 App 对应任务)
|
||||||
|
@app.task(name="partition_maintenance_task")
|
||||||
|
def partition_maintenance_task():
|
||||||
|
"""
|
||||||
|
为下一个月预建所有分区表的分区。
|
||||||
|
- 检查是否已存在目标分区,幂等执行
|
||||||
|
- 失败时发送 Sentry 告警
|
||||||
|
"""
|
||||||
|
tables = [
|
||||||
|
("follow_logs", "created_at"),
|
||||||
|
("property_photos", "created_at"),
|
||||||
|
("permission_change_logs", "operated_at"),
|
||||||
|
("login_attempts", "attempted_at"),
|
||||||
|
("public.platform_audit_logs", "created_at"),
|
||||||
|
]
|
||||||
|
next_month = date.today().replace(day=1) + relativedelta(months=1)
|
||||||
|
month_start = next_month
|
||||||
|
month_end = next_month + relativedelta(months=1)
|
||||||
|
|
||||||
|
for table, _key in tables:
|
||||||
|
suffix = month_start.strftime("%Y_%m")
|
||||||
|
part_name = f"{table.replace('.', '_')}_{suffix}"
|
||||||
|
sql = f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {part_name}
|
||||||
|
PARTITION OF {table}
|
||||||
|
FOR VALUES FROM ('{month_start}') TO ('{month_end}');
|
||||||
|
"""
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(sql)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Celery Beat 配置**(`celery.py`):
|
||||||
|
```python
|
||||||
|
app.conf.beat_schedule["partition_maintenance_task"] = {
|
||||||
|
"task": "partition_maintenance_task",
|
||||||
|
"schedule": crontab(day_of_month=1, hour=1, minute=0), # 每月1日 01:00 UTC+8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **注意**:每张分区表均保留一个 `_default` 默认分区作为兜底,防止任务失败时写入报错。`_default` 分区数据应在运维 SOP 中周期性检查(有数据则说明提前建分区失败)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、必须在开发启动前明确的数据架构决策
|
||||||
|
|
||||||
|
| 决策项 | 推荐方案 | 风险 |
|
||||||
|
|-------|---------|------|
|
||||||
|
| 小区数据来源 | 预导入基础数据(安居客/链家 API)+ 支持手动新增兜底 | 高:影响录入体验 |
|
||||||
|
| 私盘可见范围 | 录入人所在门店可见(综合业务需求) | 需与权限模块约定 |
|
||||||
|
| 号码查看权限 | 角色级控制:经纪人限查自己相关房源,店长无限制 | 需合规确认 |
|
||||||
|
| 重复房源主键 | 主键:手机号 hash;辅助:(小区+楼栋+单元+房号)组合 | 需双重校验 |
|
||||||
|
| 跟进目的枚举 | 存 lookup_items 表,运营可维护 | 初始化数据需提前收集 |
|
||||||
|
| 手机号加密算法 | AES-256-GCM,密钥存 Django settings(生产用 Vault) | 密钥管理需单独规划 |
|
||||||
|
|
||||||
|
## 十、文档治理与 ADR 联动规则
|
||||||
|
|
||||||
|
- 任何会影响数据模型边界的变更(新增/删除表、关键约束变更、索引策略调整、枚举口径变更)必须先在 `ADR.md` 记录为 `accepted`,再修改本文件与子文档。
|
||||||
|
- 若既有数据决策被替代,必须在 `ADR.md` 增加 `superseded` 记录并指向新 ADR,禁止直接覆盖旧结论。
|
||||||
|
- 提交 PR 时,凡涉及 `DATA_MODEL/*` 结构性变更,PR 描述必须包含 ADR ID(格式:`ADR-YYYYMMDD-XXX`)。
|
||||||
|
- `DATA_MODEL.md` 只维护全局索引与规则;表级 DDL 以各子文档为权威源,避免双写漂移。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档为 Fonrey 系统 DATA MODEL v1.5,随 PRD 与 ADR 迭代同步更新。*
|
||||||
|
*下一步建议:API 接口规范(URL 设计 + Request/Response Schema)*
|
||||||
581
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||||
|
# Fonrey — 客源管理数据模型(DATA_MODEL_CLIENT)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/client/` — 私客、公客、成交客、跟进记录、带看、智能配房
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **Client(客源)**:有购房/租房意向或历史成交记录的客户。核心实体,与房源(Property)是系统业务闭环的两端。
|
||||||
|
- **客源类型**:
|
||||||
|
- **私客(private)**:经纪人独占跟进的意向客户,是本期核心。
|
||||||
|
- **公客(public)**:私客超时未跟进或手动转公后,进入全公司共享客源池。
|
||||||
|
- **成交客(transacted)**:已完成购房/租房成交的客户,用于复购/转介绍跟进。
|
||||||
|
- **ClientContact(联系人)**:一个客源可有多个联系人,每个联系人有独立手机号。手机号加密存储,用于重复检测(「私客与成交客重复」)。
|
||||||
|
- **ClientRequirement(需求信息)**:购房/租房的详细偏好。一个客源可同时有「二手」「新房」「租房」三种需求类型(分别对应独立的需求记录)。
|
||||||
|
- **ClientFollowLog(跟进日志)**:经纪人与客户每次沟通的书面记录,是客源活跃度计算的数据来源。
|
||||||
|
- **Viewing(带看记录)**:与 Property 模块共享此表,记录经纪人带客户看房的过程。见主 DATA_MODEL.md 3.17 节。
|
||||||
|
- **ClientPropertyMatch(智能配房)**:系统按需求自动匹配的房源列表,分「录客配房」和「系统配房」两种来源。
|
||||||
|
- **ClientFavoriteFolder(收藏夹)**:经纪人自定义的客源分组收藏夹。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **私客手机号唯一性**:录入联系人手机号时,系统通过 `phone_hash` 检测是否与现有私客/成交客/公客重复,并在列表顶部提示重复数量。
|
||||||
|
2. **活跃度计算**:系统根据「最后跟进日期」自动计算客源活跃度,分为:新配偶(新建)/ 7日活跃 / 30日活跃 / 90日活跃 / 即将过期 / 无效。具体阈值由运营配置。
|
||||||
|
3. **私客自动转公规则**:超过配置天数(如 30 天)无跟进记录,系统自动将私客标记为公客(`transfer_to_public_type = 'auto'`)。
|
||||||
|
4. **状态机**:客源状态有严格流转规则(见第四章),不可跳过转台。
|
||||||
|
5. **跟进目的枚举**:由 `lookup_items` 表维护,运营可配置,当前已知 23 项(见 Story 8)。
|
||||||
|
6. **号码查看审计**:查看联系人明文号码需记录 `client_follow_logs`(`log_type = 'sensitive_view'`),不可删除。
|
||||||
|
7. **需求类型独立存储**:同一客源可同时有「二手购房」「租房」两类需求,分别存储在独立需求记录中,由 `client_requirements.requirement_type` 区分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (客源主表)
|
||||||
|
│
|
||||||
|
├── 1:N ── ClientContact (联系人,多个号码)
|
||||||
|
├── 1:N ── ClientRequirement (需求信息,可多类型)
|
||||||
|
├── 1:N ── ClientFollowLog (跟进日志,高写入频率)
|
||||||
|
├── 1:N ── ClientViewing (带看预约)
|
||||||
|
├── 1:N ── ClientPropertyMatch (智能配房结果)
|
||||||
|
├── 1:1 ── ClientActivityCache (活跃度缓存,异步计算)
|
||||||
|
├── N:M ── ClientFavoriteFolder (通过 client_folder_items 关联)
|
||||||
|
└── 1:N ── ClientStatusLog (状态变更日志,不可删)
|
||||||
|
|
||||||
|
ClientFavoriteFolder
|
||||||
|
└── 1:N ── ClientFolderItem (收藏夹中的客源)
|
||||||
|
|
||||||
|
Staff (员工)
|
||||||
|
├── first_recorder_id → Client (首录人)
|
||||||
|
└── owner_id → Client (归属人)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 clients — 客源主表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_no | VARCHAR(30) | UNIQUE, NOT NULL | 系统生成的客源编号,格式由运营配置(如 KY20260424001) |
|
||||||
|
| client_type | VARCHAR(20) | NOT NULL DEFAULT 'private' | `private`=私客 / `public`=公客 / `transacted`=成交客 |
|
||||||
|
| status | VARCHAR(20) | NOT NULL DEFAULT 'buying' | 见下方枚举 |
|
||||||
|
| grade | VARCHAR(5) | NOT NULL DEFAULT 'C' | `A`=A急迫 / `B`=较强 / `C`=一般 / `D`=较弱 / `E`=暂不关注 |
|
||||||
|
| property_usage | VARCHAR(30) | NOT NULL DEFAULT 'residential' | `residential`=住宅 / `villa`=别墅 / `commercial_residential`=商住 / `shop`=商铺 / `office`=写字楼 / `other`=其他 |
|
||||||
|
| buying_purpose | VARCHAR(20)[] | | 购房目的多选:`rigid`=刚需 / `investment`=投资 / `school_district`=学区 / `upgrade`=改善 / `commercial`=商用 / `other`=其他 |
|
||||||
|
| payment_method | VARCHAR(30) | | `full`=全额 / `mortgage`=商业贷款 / `mortgage_fund`=商贷+公积金 / `fund`=公积金 |
|
||||||
|
| properties_owned | VARCHAR(20) | | `none`=无 / `local_none`=本地无外地有 / `local_has`=本地有 |
|
||||||
|
| has_loan_record | BOOLEAN | | 有无贷款记录 |
|
||||||
|
| id_type | VARCHAR(20) | | 证件类型:`id_card` / `passport` / `hk_macao` / `other` |
|
||||||
|
| id_number_enc | BYTEA | | 证件号码(AES 加密) |
|
||||||
|
| source | VARCHAR(50) | | 客户来源(lookup_items 维护) |
|
||||||
|
| remarks | TEXT | | 备注,最多200字 |
|
||||||
|
| is_starred | BOOLEAN | NOT NULL DEFAULT FALSE | 是否收藏(快速标记,详细收藏夹用 client_folder_items) |
|
||||||
|
| is_pinned | BOOLEAN | NOT NULL DEFAULT FALSE | 是否置顶(列表顶部置顶) |
|
||||||
|
| is_big_value | BOOLEAN | NOT NULL DEFAULT FALSE | 是否大价值客户(影响筛选展示) |
|
||||||
|
| is_protected | BOOLEAN | NOT NULL DEFAULT FALSE | 是否保护客(影响转公逻辑) |
|
||||||
|
| prefers_new_house | BOOLEAN | | 偏好新房(用于筛选) |
|
||||||
|
| transfer_to_public_type | VARCHAR(20) | | 转公客方式:`manual`=手动转公 / `auto`=自动转公(超时) / `marketing_jump`=营销客跳公 / `resource_public`=资料客素公 |
|
||||||
|
| transferred_public_at | TIMESTAMPTZ | | 进入公客池时间 |
|
||||||
|
| invalid_reason | VARCHAR(30) | | 无效原因:`invalid_phone`=号码无效 / `peer_agent`=同行 / `ad`=广告推销 / `no_intent`=无意向 / `other` |
|
||||||
|
| invalidated_at | TIMESTAMPTZ | | 标记无效时间 |
|
||||||
|
| transacted_at | DATE | | 成交日期 |
|
||||||
|
| transacted_property_id | UUID | FK→properties, SET NULL | 成交关联的房源 |
|
||||||
|
| transacted_price | NUMERIC(12,2) | | 成交价格(万元) |
|
||||||
|
| transacted_type | VARCHAR(20) | | 成交类型:`bought`=我购 / `rented`=我租 |
|
||||||
|
| transacted_property_type | VARCHAR(20) | | 成交房源类型:`second_hand`=二手 / `new_house`=新房 |
|
||||||
|
| first_recorder_id | UUID | FK→staff, SET NULL | 首录人 |
|
||||||
|
| owner_id | UUID | FK→staff, SET NULL | 归属人(私客独占跟进人) |
|
||||||
|
| org_unit_id | UUID | FK→org_units, SET NULL | 归属部门(冗余,加速筛选) |
|
||||||
|
| activity_level | VARCHAR(20) | | `new_matched`=新配偶 / `active_7d` / `active_30d` / `active_90d` / `expiring` / `frozen` / `invalid`(异步计算)|
|
||||||
|
| last_active_at | TIMESTAMPTZ | | 最后有效跟进时间(触发器维护) |
|
||||||
|
| last_follow_at | TIMESTAMPTZ | | 最后跟进时间(冗余,列表排序用) |
|
||||||
|
| commission_date | DATE | | 委托日期 |
|
||||||
|
| entrust_count | SMALLINT | NOT NULL DEFAULT 1 | 委托次数(成交后再委托则累加) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;NULL=未删除,非NULL=已软删除 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) |
|
||||||
|
| updated_by | UUID | FK→staff, SET NULL | 最后修改人(操作员工) |
|
||||||
|
| version | INTEGER | NOT NULL DEFAULT 1 | 乐观锁版本号;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_clients_client_no ON clients(client_no) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_type_status ON clients(client_type, status) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_owner ON clients(owner_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_org_unit ON clients(org_unit_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_grade ON clients(grade) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_transferred_at ON clients(transferred_public_at DESC) WHERE client_type = 'public';
|
||||||
|
CREATE INDEX idx_clients_last_follow ON clients(last_follow_at DESC NULLS LAST) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 client_contacts — 联系人表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | 所属客源(关联 clients 表,联系人随客源级联删除) |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | 联系人1为主联系人(sort_order=0) |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 联系人姓名 |
|
||||||
|
| gender | VARCHAR(10) | NOT NULL DEFAULT 'male' | `male`=先生 / `female`=女士 |
|
||||||
|
| phone_enc | BYTEA | NOT NULL | AES-256-GCM 加密手机号(电话1) |
|
||||||
|
| phone_hash | VARCHAR(64) | NOT NULL | SHA-256 哈希(重复检测) |
|
||||||
|
| phone_country_code | VARCHAR(10) | NOT NULL DEFAULT '+86' | 国际区号 |
|
||||||
|
| phone_is_invalid | BOOLEAN | NOT NULL DEFAULT FALSE | 是否被标记为无效号码 |
|
||||||
|
| phone2_enc | BYTEA | | 备用电话2 |
|
||||||
|
| phone2_hash | VARCHAR(64) | | 备用电话2哈希(SHA-256,用于重复检测) |
|
||||||
|
| wechat | VARCHAR(100) | | 微信号 |
|
||||||
|
| qq | VARCHAR(20) | | QQ号 |
|
||||||
|
| remarks | VARCHAR(200) | | 联系人备注,最多200字 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;NULL=未删除(不影响客源本身) |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
-- 关键:手机号哈希全局唯一索引(用于重复客源检测)
|
||||||
|
CREATE INDEX idx_client_contacts_phone_hash ON client_contacts(phone_hash) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_contacts_phone2_hash ON client_contacts(phone2_hash) WHERE phone2_hash IS NOT NULL AND deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_contacts_client ON client_contacts(client_id) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:
|
||||||
|
- `sort_order = 0` 的联系人为主联系人,姓名用于客源姓名显示
|
||||||
|
- 手机号标记无效(`phone_is_invalid = TRUE`)时,不影响记录存在,但该号码不再参与重复检测
|
||||||
|
- 联系人软删除后客源仍保留,但若所有联系人均被删则客源实际上无有效号码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 client_requirements — 需求信息表
|
||||||
|
|
||||||
|
一个客源可同时有多类需求(二手购房、新房、租房),每类需求独立一条记录。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | 所属客源(关联 clients 表,需求随客源级联删除) |
|
||||||
|
| requirement_type | VARCHAR(20) | NOT NULL | `second_hand`=二手 / `new_house`=新房 / `rental`=租房 |
|
||||||
|
| is_primary | BOOLEAN | NOT NULL DEFAULT TRUE | 是否为主需求(用于列表展示) |
|
||||||
|
| budget_min | NUMERIC(12,2) | | 最低预算(万元/元,依据需求类型) |
|
||||||
|
| budget_max | NUMERIC(12,2) | | 最高预算 |
|
||||||
|
| area_min | NUMERIC(8,2) | | 最小面积(㎡) |
|
||||||
|
| area_max | NUMERIC(8,2) | | 最大面积 |
|
||||||
|
| bedroom_counts | SMALLINT[] | | 可接受卧室数:如 [2,3](多选) |
|
||||||
|
| floor_preferences | VARCHAR(20)[] | | 楼层偏好多选:`no_first`=不要一层 / `low`=低楼层 / `mid`=中楼层 / `high`=高楼层 / `no_top`=不要顶层 |
|
||||||
|
| orientations | VARCHAR(10)[] | | 朝向多选:`east`/`south`/`west`/`north` |
|
||||||
|
| decorations | VARCHAR(10)[] | | 装修偏好多选(枚举同 properties.decoration) |
|
||||||
|
| building_age_ranges | VARCHAR(20)[] | | 楼龄多选:`within_5y`/`5_10y`/`10_15y`/`15_20y`/`over_20y` |
|
||||||
|
| intent_district_ids | UUID[] | | 意向行政区 ID 数组 |
|
||||||
|
| intent_business_area_ids | UUID[] | | 意向商圈 ID 数组 |
|
||||||
|
| intent_complex_names | TEXT | | 意向小区(文本,逗号分隔,最多500字) |
|
||||||
|
| transportation | VARCHAR(50) | | 交通要求(最多50字) |
|
||||||
|
| intent_school_names | TEXT | | 意向学校(文本,逗号分隔) |
|
||||||
|
| school_enrollment_date | DATE | | 入学时间(月份精度,取该月1日存储) |
|
||||||
|
| traffic_preference | TEXT | | 交通备注 |
|
||||||
|
| requirement_notes | VARCHAR(200) | | 需求备注(最多200字) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_requirements_client ON client_requirements(client_id);
|
||||||
|
CREATE INDEX idx_client_requirements_type ON client_requirements(requirement_type, client_id);
|
||||||
|
-- 智能配房时按预算/面积范围查询
|
||||||
|
CREATE INDEX idx_client_requirements_budget ON client_requirements(budget_min, budget_max);
|
||||||
|
CREATE INDEX idx_client_requirements_area ON client_requirements(area_min, area_max);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 client_follow_logs — 客源跟进日志
|
||||||
|
|
||||||
|
> 与 `follow_logs`(房源跟进)结构类似,独立存储以避免跨模块混淆。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | 所属客源(关联 clients 表,跟进日志随客源级联删除) |
|
||||||
|
| log_type | VARCHAR(30) | NOT NULL | 见下方枚举 |
|
||||||
|
| purpose | VARCHAR(50) | | 跟进目的(lookup_items 维护,23项) |
|
||||||
|
| content | TEXT | | 跟进内容(最少6字,最多500字) |
|
||||||
|
| log_tag | VARCHAR(50) | | 跟进标签:`has_recording`=有录音 / `has_photo`=有图片 / `not_satisfied`=对房源不满意 / `still_considering`=还在考虑 / `ready_to_deposit`=可交定金 |
|
||||||
|
| change_detail | JSONB | | 修改跟进专用,格式:`{"field": "grade", "old": "C", "new": "B", "label": "等级"}` |
|
||||||
|
| is_public | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=仅本人及上级可见 |
|
||||||
|
| is_deletable | BOOLEAN | NOT NULL DEFAULT TRUE | 敏感信息查看类型为 FALSE,不可删除 |
|
||||||
|
| operator_id | UUID | FK→staff, SET NULL | 操作人 |
|
||||||
|
| operator_snapshot | JSONB | | `{name, store_group, role}`(防止人员调动后显示异常) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;仅 is_deletable=TRUE 时可软删 |
|
||||||
|
|
||||||
|
**log_type 枚举**:
|
||||||
|
```
|
||||||
|
written = 写入跟进(经纪人主动写)
|
||||||
|
modified = 修改跟进(字段变更自动生成)
|
||||||
|
sensitive_view= 敏感信息查看(查看号码等,不可删)
|
||||||
|
other = 其他跟进(系统自动:新增私客/状态变更等)
|
||||||
|
system = 系统日志
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_follow_logs_client_time ON client_follow_logs(client_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_follow_logs_type ON client_follow_logs(client_id, log_type, created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_follow_logs_operator ON client_follow_logs(operator_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
-- 不可删记录(合规审计)
|
||||||
|
CREATE INDEX idx_client_follow_sensitive ON client_follow_logs(client_id, created_at DESC) WHERE log_type = 'sensitive_view';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 client_follow_log_attachments — 跟进附件
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| follow_log_id | UUID | NOT NULL, FK→client_follow_logs, CASCADE | 所属跟进日志(附件随日志级联删除) |
|
||||||
|
| file_key | TEXT | NOT NULL | R2/S3 存储路径 |
|
||||||
|
| file_name | VARCHAR(255) | NOT NULL | 原始文件名(用于展示和下载) |
|
||||||
|
| file_size | INTEGER | NOT NULL | bytes,最大 20MB |
|
||||||
|
| file_type | VARCHAR(10) | CHECK | `bmp`/`jpg`/`png`/`gif` |
|
||||||
|
| has_location | BOOLEAN | NOT NULL DEFAULT FALSE | 是否含 GPS 位置信息 |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | 附件排序顺序 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 client_viewings — 带看记录(客源侧视图)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, RESTRICT | 所属客源(带看记录仅软删除,不随客源删除) |
|
||||||
|
| property_id | UUID | NOT NULL, FK→properties, RESTRICT | 带看房源(房源删除时保留带看记录) |
|
||||||
|
| viewing_type | VARCHAR(20) | NOT NULL DEFAULT 'viewing' | `appointment`=预约 / `viewing`=带看 / `revisit`=复看 / `empty`=空看 |
|
||||||
|
| agent_id | UUID | FK→staff, SET NULL | 主带看经纪人 |
|
||||||
|
| companion_ids | UUID[] | | 陪看人员 ID 数组(最多5人) |
|
||||||
|
| cooperator_ids | UUID[] | | 合作带看人 ID 数组(最多5人) |
|
||||||
|
| scheduled_at | TIMESTAMPTZ | | 预约时间 |
|
||||||
|
| viewing_start_at | TIMESTAMPTZ | | 实际带看开始时间 |
|
||||||
|
| viewing_end_at | TIMESTAMPTZ | | 结束时间 |
|
||||||
|
| situation | TEXT | | 带看情况(必填,≥6字) |
|
||||||
|
| client_intent | VARCHAR(20) | | 客户意向:`interested`=感兴趣 / `not_interested`=不感兴趣 / `negotiating`=谈判中 / `cancelled`=取消 |
|
||||||
|
| viewing_progress | SMALLINT | | 带看进度(1=一看,2=二看...,冗余字段,触发器维护) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;带看记录可软删除 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_viewings_client ON client_viewings(client_id, viewing_start_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_viewings_property ON client_viewings(property_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_viewings_agent ON client_viewings(agent_id) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 client_property_matches — 智能配房
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | 所属客源 |
|
||||||
|
| property_id | UUID | NOT NULL, FK→properties, CASCADE | 匹配的房源 |
|
||||||
|
| match_source | VARCHAR(20) | NOT NULL DEFAULT 'recorded' | `recorded`=录客配房(基于录入需求) / `system`=系统配房(算法推荐) |
|
||||||
|
| match_group | VARCHAR(30) | | 分组:`quality_layout`=优质户型 / `price_reduced`=降价 / `hot`=热门 / `newly_listed`=新上 |
|
||||||
|
| match_score | NUMERIC(5,2) | | 匹配度评分(0-100) |
|
||||||
|
| match_reasons | JSONB | | 匹配原因详情,格式:`[{"key": "budget", "match": true}, ...]` |
|
||||||
|
| status | VARCHAR(20) | NOT NULL DEFAULT 'suggested' | `suggested`=待推送 / `shared`=已分享 / `rejected`=已反馈不合适 / `viewed`=客户已查看 |
|
||||||
|
| shared_at | TIMESTAMPTZ | | 分享时间 |
|
||||||
|
| feedback | VARCHAR(50) | | 反馈原因(lookup_items 维护) |
|
||||||
|
| calculated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 配房计算时间 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 触发配房操作的员工(录客配房时记录,系统配房可为NULL) |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_client_matches_pair ON client_property_matches(client_id, property_id);
|
||||||
|
CREATE INDEX idx_client_matches_client ON client_property_matches(client_id, match_source, match_group);
|
||||||
|
CREATE INDEX idx_client_matches_status ON client_property_matches(client_id, status) WHERE status != 'rejected';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 client_status_logs — 状态变更日志(不可删)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, RESTRICT | 所属客源(状态日志永久保留,RESTRICT 防止删除客源) |
|
||||||
|
| change_type | VARCHAR(30) | NOT NULL | `status_change`=改状态 / `grade_change`=改等级 / `to_public`=转公客 / `to_transacted`=转成交 / `to_invalid`=转无效 / `owner_change`=改归属人 / `source_change`=改来源 |
|
||||||
|
| old_value | JSONB | | 变更前快照,格式:`{"status": "buying", "label": "求购"}` |
|
||||||
|
| new_value | JSONB | | 变更后快照 |
|
||||||
|
| reason | TEXT | | 变更理由(改状态必填,最多200字) |
|
||||||
|
| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | 操作人(必填,状态变更审计用) |
|
||||||
|
| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 操作时间(系统自动记录) |
|
||||||
|
| ⚠️ 无 deleted_at | — | — | 此表记录**不可删除** |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_status_logs_client ON client_status_logs(client_id, operated_at DESC);
|
||||||
|
CREATE INDEX idx_client_status_logs_type ON client_status_logs(change_type, operated_at DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 client_favorite_folders — 私客收藏夹
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff, CASCADE | 收藏夹所属经纪人 |
|
||||||
|
| name | VARCHAR(10) | NOT NULL | 收藏夹名称,最多10字 |
|
||||||
|
| is_default | BOOLEAN | NOT NULL DEFAULT FALSE | 系统默认收藏夹 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 收藏夹显示顺序(升序) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;NULL=未删除 |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_favorite_folders_staff ON client_favorite_folders(staff_id) WHERE deleted_at IS NULL;
|
||||||
|
-- 每个经纪人只能有一个默认收藏夹
|
||||||
|
CREATE UNIQUE INDEX idx_favorite_folders_default ON client_favorite_folders(staff_id) WHERE is_default = TRUE AND deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 client_folder_items — 收藏夹中的客源
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
| ----------- | ---------------------- | --------------------------------------------- | ---- |
|
||||||
|
| folder_id | UUID | NOT NULL, FK→client_favorite_folders, CASCADE | 所属收藏夹 |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | 被收藏的客源 |
|
||||||
|
| added_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 加入收藏夹时间 |
|
||||||
|
| PRIMARY KEY | (folder_id, client_id) | | 联合主键(同一客源在同一收藏夹只能出现一次) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_folder_items_client ON client_folder_items(client_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.11 client_school_preferences — 意向学校(多对多)
|
||||||
|
|
||||||
|
> 单独拆表便于学校搜索,避免文本字段模糊查询。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| requirement_id | UUID | NOT NULL, FK→client_requirements, CASCADE | 所属需求(意向学校随需求级联删除) |
|
||||||
|
| school_id | UUID | FK→schools, SET NULL | 从学校表选择,允许为 NULL(自由输入) |
|
||||||
|
| school_name | VARCHAR(100) | NOT NULL | 学校名称(当 school_id 为 NULL 时为手动输入) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_school_prefs_requirement ON client_school_preferences(requirement_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、枚举常量
|
||||||
|
|
||||||
|
### clients.status(客源状态)
|
||||||
|
|
||||||
|
```
|
||||||
|
buying = 求购(私客活跃态)
|
||||||
|
renting = 求租(私客活跃态)
|
||||||
|
buy_or_rent = 租购(私客活跃态)
|
||||||
|
suspended = 暂缓(暂时无需求,不计入活跃统计)
|
||||||
|
bought = 已购(成交客:我购)
|
||||||
|
rented_done = 已租(成交客:我租)
|
||||||
|
public = 公客(已转入公客池)
|
||||||
|
invalid = 无效(号码无效/无意向等)
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
```
|
||||||
|
buying/renting/buy_or_rent
|
||||||
|
→ suspended (改状态操作,可逆)
|
||||||
|
→ public (手动转公 or 超时自动转公,不可逆)
|
||||||
|
→ bought/rented_done (转成交,不可逆)
|
||||||
|
→ invalid (转无效,需经理审批后可恢复)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clients.grade(等级)
|
||||||
|
|
||||||
|
```
|
||||||
|
A = A(急迫)
|
||||||
|
B = B(较强)
|
||||||
|
C = C(一般,默认值)
|
||||||
|
D = D(较弱)
|
||||||
|
E = E(暂不关注)
|
||||||
|
```
|
||||||
|
|
||||||
|
### client_status_logs.change_type(变更类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
status_change = 改状态(含改等级时同时改状态的情况)
|
||||||
|
grade_change = 改等级
|
||||||
|
to_public = 转公客(manual=手动 or auto=自动)
|
||||||
|
to_transacted = 转成交(记录成交信息)
|
||||||
|
to_invalid = 转无效(含无效原因)
|
||||||
|
owner_change = 改归属人
|
||||||
|
source_change = 改来源
|
||||||
|
merge = 合并客源(被合并的记录保留日志)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clients.activity_level(活跃度分层,系统计算)
|
||||||
|
|
||||||
|
| 值 | 含义 | 触发条件(示例,以运营配置为准) |
|
||||||
|
| ------------- | ------ | ------------------ |
|
||||||
|
| `new_matched` | 新匹配 | 录入后 3 天内 |
|
||||||
|
| `active_7d` | 7日活跃 | 最后跟进在 7 天内 |
|
||||||
|
| `active_30d` | 30日活跃 | 最后跟进在 30 天内 |
|
||||||
|
| `active_90d` | 90日活跃 | 最后跟进在 90 天内 |
|
||||||
|
| `expiring` | 即将过期 | 距自动转公还有 N 天 |
|
||||||
|
| `frozen` | 冻结(暂缓) | status = suspended |
|
||||||
|
| `invalid` | 无效 | status = invalid |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、查询模式
|
||||||
|
|
||||||
|
### 5.1 私客列表页(求购 Tab)核心查询
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 典型:当前经纪人名下 + 求购状态 + 等级筛选 + 按最后跟进排序
|
||||||
|
SELECT c.id, c.status, c.grade, c.activity_level,
|
||||||
|
c.last_follow_at, c.commission_date, c.buying_purpose,
|
||||||
|
cc.name AS contact_name, -- JOIN 主联系人
|
||||||
|
s.name AS owner_name, ou.name AS org_unit_name,
|
||||||
|
COUNT(cpm.id) AS match_count -- 智能配房数量
|
||||||
|
FROM clients c
|
||||||
|
JOIN client_contacts cc ON cc.client_id = c.id AND cc.sort_order = 0 AND cc.deleted_at IS NULL
|
||||||
|
JOIN staff s ON s.id = c.owner_id
|
||||||
|
JOIN org_units ou ON ou.id = c.org_unit_id
|
||||||
|
LEFT JOIN client_property_matches cpm ON cpm.client_id = c.id AND cpm.status != 'rejected'
|
||||||
|
WHERE c.client_type = 'private'
|
||||||
|
AND c.owner_id = :current_staff_id -- 与我相关
|
||||||
|
AND c.status IN ('buying', 'buy_or_rent')
|
||||||
|
AND c.deleted_at IS NULL
|
||||||
|
GROUP BY c.id, cc.name, s.name, ou.name
|
||||||
|
ORDER BY c.last_follow_at DESC NULLS LAST
|
||||||
|
LIMIT 20 OFFSET :offset;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 重复客源检测(录入/编辑时触发)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 手机号哈希碰撞检测(私客、成交客、公客三池同时检查)
|
||||||
|
SELECT c.id, c.client_type, c.status, c.client_no,
|
||||||
|
cc.name AS contact_name
|
||||||
|
FROM client_contacts cc
|
||||||
|
JOIN clients c ON cc.client_id = c.id
|
||||||
|
WHERE cc.phone_hash = :new_phone_hash
|
||||||
|
AND cc.deleted_at IS NULL
|
||||||
|
AND c.deleted_at IS NULL
|
||||||
|
AND c.status != 'invalid';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 活跃度批量更新(Celery 定时任务,每日凌晨执行)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 更新活跃度(以7日活跃为例)
|
||||||
|
UPDATE clients
|
||||||
|
SET activity_level = 'active_7d',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE client_type = 'private'
|
||||||
|
AND status NOT IN ('invalid', 'public', 'bought', 'rented_done')
|
||||||
|
AND last_follow_at >= NOW() - INTERVAL '7 days'
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 私客自动转公(超时无跟进,Celery 定时任务)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 查询应自动转公的私客(阈值由运营配置,假设30天)
|
||||||
|
SELECT id FROM clients
|
||||||
|
WHERE client_type = 'private'
|
||||||
|
AND status IN ('buying', 'renting', 'buy_or_rent')
|
||||||
|
AND last_follow_at < NOW() - INTERVAL '30 days'
|
||||||
|
AND is_protected = FALSE
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
-- 后续在 Application 层批量更新 client_type='public', transfer_to_public_type='auto'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、触发器
|
||||||
|
|
||||||
|
### 6.1 last_follow_at 自动维护
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 每次写入跟进日志时,自动更新 clients.last_follow_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_client_last_follow()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.log_type = 'written' THEN
|
||||||
|
UPDATE clients
|
||||||
|
SET last_follow_at = NEW.created_at,
|
||||||
|
last_active_at = NEW.created_at,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = NEW.client_id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_client_last_follow
|
||||||
|
AFTER INSERT ON client_follow_logs
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_client_last_follow();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 viewing_progress 自动维护
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 每次新增带看记录时,自动更新 clients 的带看进度冗余字段
|
||||||
|
CREATE OR REPLACE FUNCTION update_client_viewing_progress()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE clients
|
||||||
|
SET updated_at = NOW()
|
||||||
|
WHERE id = NEW.client_id;
|
||||||
|
-- Application 层根据 COUNT(viewings) 计算具体进度
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_client_viewing_progress
|
||||||
|
AFTER INSERT ON client_viewings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_client_viewing_progress();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、禁止操作
|
||||||
|
|
||||||
|
- ❌ **严禁硬删除 clients 记录**:无效/转公客/成交客均通过 status 和 soft delete 处理,历史跟进/带看依赖外键
|
||||||
|
- ❌ **严禁删除 client_status_logs**:状态变更为不可变审计日志
|
||||||
|
- ❌ **严禁删除 log_type='sensitive_view' 的跟进记录**:必须通过 `is_deletable=FALSE` 约束在应用层拦截
|
||||||
|
- ❌ **严禁明文存储联系人手机号**:必须走 `EncryptedPhoneField`,`phone_hash` 用于索引和重复检测
|
||||||
|
- ❌ **严禁跳过状态机流转**:如私客不可直接跳过「求购」变为「无效」而不生成 status log
|
||||||
|
- ❌ **严禁在没有 `client_type` 过滤的情况下查询客源列表**:私客/公客/成交客数据量均较大,必须按类型隔离查询
|
||||||
|
- ❌ **严禁查询 clients 时不带 `deleted_at IS NULL`**:软删除过滤必须存在
|
||||||
555
Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||||
|
# Fonrey — 楼盘与区域数据模型(DATA_MODEL_COMPLEX)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/complex/` — 楼盘/小区、楼栋、结构(楼层+房号)、区域、学校
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **Complex(楼盘/小区)**:房源录入的基础底座。每套房源必须归属于某一楼盘。楼盘数据由运营/数据管理员集中维护,质量直接影响房源录入效率和搜索精度。
|
||||||
|
- **Building(楼栋/单元)**:楼盘下的物理楼栋,是组织房源位置的第二级。一个楼盘可有多个楼栋(如「1号楼」「2栋2单元」)。
|
||||||
|
- **RoomUnit(房号/结构单元)**:楼栋内特定楼层的某个房间标识,是房源定位的最细粒度。支持「标准结构」(经运营标准化)和「非标结构」(未归一化)两类。
|
||||||
|
- **District(城区/行政区)**:行政区划,如静安区、闵行区。
|
||||||
|
- **BusinessArea(商圈/板块)**:商圈是区域内的细分市场区域,如「南京西路商圈」,一个楼盘可跨多个商圈。
|
||||||
|
- **School(学校)**:楼盘对口学校,是买家购房决策的核心关注点。一个楼盘可关联多所学校,一所学校可对口多个楼盘。
|
||||||
|
- **MetroLine / MetroStation(地铁线路/站点)**:楼盘与最近地铁站的距离关系,用于通勤筛选。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **楼盘名称不可在编辑页修改**:楼盘名称(`name`)变更须通过「合并楼盘」或「申请流程」处理,防止经纪人随意改名造成数据混乱。
|
||||||
|
2. **数据锁定机制**:楼盘有 4 类锁(楼栋锁/房号锁/信息锁/标准房号锁),锁定后对应数据只有管理员可解锁修改。
|
||||||
|
3. **非标结构处理**:未与标准结构关联的房号为「非标」,系统记录非标数量,引导运营逐步消除。
|
||||||
|
4. **搜索依赖全文检索**:楼盘名称、别名、地址需维护 `search_vector`(`tsvector`)以支持模糊搜索和联想补全。
|
||||||
|
5. **地理坐标优先级**:楼盘坐标是区域聚合展示(地图找房)的核心数据,完整度目标 ≥ 90%。
|
||||||
|
6. **学校关联影响房源**:从楼盘详情删除对口学校,会级联删除该楼盘下所有房源的对应学区标注。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
District (城区/行政区)
|
||||||
|
└── 1:N ── BusinessArea (商圈/板块)
|
||||||
|
└── N:M ── Complex (through complex_business_areas)
|
||||||
|
|
||||||
|
Complex (楼盘)
|
||||||
|
├── N:M ── BusinessArea (through complex_business_areas)
|
||||||
|
├── N:M ── School (through complex_schools)
|
||||||
|
├── N:M ── MetroStation (through complex_metro_stations, 附带距离)
|
||||||
|
├── 1:N ── Building (楼栋/单元)
|
||||||
|
│ └── 1:N ── RoomUnit (楼层+房号)
|
||||||
|
├── 1:N ── ComplexPhoto (楼盘照片:楼盘图/户型图/VR)
|
||||||
|
├── 1:N ── ComplexAttachment(附件)
|
||||||
|
├── 1:N ── ComplexPriceTrend(价格走势,月度)
|
||||||
|
└── 1:N ── ComplexAlias (别名)
|
||||||
|
|
||||||
|
MetroLine (地铁线路)
|
||||||
|
└── 1:N ── MetroStation (站点)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 districts — 城区/行政区
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| city | VARCHAR(50) | NOT NULL | 所属城市(支持多城市扩展,如「上海」「北京」) |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 行政区名称,如「静安区」 |
|
||||||
|
| short_name | VARCHAR(20) | | 简称,如「静安」 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 列表展示排序 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_districts_city_name ON districts(city, name) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 business_areas — 商圈/板块
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| district_id | UUID | NOT NULL, FK→districts, RESTRICT | 所属城区 |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 商圈名称 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 列表展示排序 |
|
||||||
|
| latitude | NUMERIC(10,7) | | 商圈中心坐标(纬度) |
|
||||||
|
| longitude | NUMERIC(10,7) | | 商圈中心坐标(经度) |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_business_areas_district ON business_areas(district_id) WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_business_areas_name ON business_areas(district_id, name);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 metro_lines — 地铁线路
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| city | VARCHAR(50) | NOT NULL | 所属城市 |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 线路名,如「1号线」 |
|
||||||
|
| color | VARCHAR(7) | | 线路颜色 HEX(如 `#E3002B`) |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 列表展示排序 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 metro_stations — 地铁站点
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| metro_line_id | UUID | NOT NULL, FK→metro_lines, CASCADE | 所属线路 |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 站名 |
|
||||||
|
| latitude | NUMERIC(10,7) | | 站点坐标 |
|
||||||
|
| longitude | NUMERIC(10,7) | | 站点坐标(经度) |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 沿线排序 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_metro_stations_line ON metro_stations(metro_line_id) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 schools — 学校
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| district_id | UUID | FK→districts, SET NULL | 所属城区 |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 学校名称 |
|
||||||
|
| type | VARCHAR(20) | | 学校类型:`primary`=小学 / `middle`=初中 / `high`=高中 / `k9`=九年一贯制 / `k12`=十二年一贯制 |
|
||||||
|
| nature | VARCHAR(20) | | 学校性质:`public`=公立 / `private`=私立 / `international`=国际学校 |
|
||||||
|
| level | VARCHAR(20) | | 学校等级:`normal`=普通 / `key`=重点 / `top`=名校 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_schools_district ON schools(district_id) WHERE is_active = TRUE;
|
||||||
|
CREATE INDEX idx_schools_name_trgm ON schools USING gin(name gin_trgm_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 complexes — 楼盘/小区(核心基础表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| name | VARCHAR(200) | NOT NULL | 标准楼盘名称,**不可在编辑页修改**(需走合并/申请流程) |
|
||||||
|
| district_id | UUID | FK→districts, SET NULL | 所属城区 |
|
||||||
|
| address | VARCHAR(500) | | 详细地址(不可在编辑页修改,需走纠错流程) |
|
||||||
|
| address_summary | VARCHAR(100) | | 概要地址(如「海波路1000弄」,可编辑) |
|
||||||
|
| latitude | NUMERIC(10,7) | | 楼盘坐标(纬度),完整度目标 ≥ 90% |
|
||||||
|
| longitude | NUMERIC(10,7) | | 地铁站经度坐标(WGS84) |
|
||||||
|
| **物业属性** | | | |
|
||||||
|
| property_usage_types | VARCHAR(20)[] | | 物业类型多选:`residential`/`villa`/`commercial_residential`/`commercial`/`office`/`other` |
|
||||||
|
| building_structure | VARCHAR(30) | | 楼栋结构枚举(运营维护):`unit_room`=单元-房号 / `other`=其他 |
|
||||||
|
| building_type | VARCHAR(20) | | 建筑类型:`slab`=板楼 / `tower`=塔楼 / `slab_tower`=板塔结合 |
|
||||||
|
| land_use_years | VARCHAR(30) | | 土地使用年限,如「70年」 |
|
||||||
|
| built_year | SMALLINT | | 竣工年份(可多选,存最早竣工年) |
|
||||||
|
| built_years | SMALLINT[] | | 竣工年份多值(楼盘分期竣工) |
|
||||||
|
| ownership_category | VARCHAR(30)[] | | 权属类别多选(运营维护枚举) |
|
||||||
|
| total_units | INTEGER | | 单元总数 |
|
||||||
|
| total_households | INTEGER | | 总户数 |
|
||||||
|
| **建设信息** | | | |
|
||||||
|
| total_floor_area | NUMERIC(12,2) | | 小区总建筑面积(m²) |
|
||||||
|
| plot_area | NUMERIC(12,2) | | 小区占地面积(m²) |
|
||||||
|
| plot_ratio | NUMERIC(5,2) | | 容积率 |
|
||||||
|
| green_rate | NUMERIC(5,2) | | 绿化率(%) |
|
||||||
|
| developer | VARCHAR(200) | | 开发商 |
|
||||||
|
| **物业信息** | | | |
|
||||||
|
| property_company | VARCHAR(200) | | 物业公司 |
|
||||||
|
| property_fee | NUMERIC(8,2) | | 物业费(元/m²/月) |
|
||||||
|
| property_phone | VARCHAR(30) | | 物业电话 |
|
||||||
|
| **停车** | | | |
|
||||||
|
| parking_total | INTEGER | | 车位总数 |
|
||||||
|
| parking_underground | INTEGER | | 地下车位数 |
|
||||||
|
| parking_ratio | VARCHAR(20) | | 停车位配比,如「100:63」 |
|
||||||
|
| **配套** | | | |
|
||||||
|
| water_type | VARCHAR(10) | | `civil`=民水 / `commercial`=商水 |
|
||||||
|
| electricity_type | VARCHAR(10) | | `civil`=民电 / `commercial`=商电 |
|
||||||
|
| has_central_heating | BOOLEAN | | 是否统一供暖 |
|
||||||
|
| has_gas | BOOLEAN | | 是否有燃气 |
|
||||||
|
| remarks | TEXT | | 备注 |
|
||||||
|
| **锁定状态** | | | |
|
||||||
|
| lock_building | BOOLEAN | NOT NULL DEFAULT FALSE | 楼栋锁(锁定后不可增删楼栋) |
|
||||||
|
| lock_room | BOOLEAN | NOT NULL DEFAULT FALSE | 房号锁 |
|
||||||
|
| lock_info | BOOLEAN | NOT NULL DEFAULT FALSE | 信息锁(锁定后基本信息只读) |
|
||||||
|
| lock_standard_room | BOOLEAN | NOT NULL DEFAULT FALSE | 标准房号锁 |
|
||||||
|
| **全文检索** | | | |
|
||||||
|
| search_vector | TSVECTOR | | 由触发器自动维护(name + alias + address) |
|
||||||
|
| **状态** | | | |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用楼盘 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;NULL=未删除,非NULL=已软删除 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) |
|
||||||
|
| updated_by | UUID | FK→staff, SET NULL | 最后更新人(操作员工) |
|
||||||
|
| version | INTEGER | NOT NULL DEFAULT 1 | 乐观锁版本号;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complexes_district ON complexes(district_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_complexes_name_trgm ON complexes USING gin(name gin_trgm_ops); -- 模糊搜索
|
||||||
|
CREATE INDEX idx_complexes_search ON complexes USING gin(search_vector); -- 全文搜索
|
||||||
|
CREATE INDEX idx_complexes_geo ON complexes(latitude, longitude) WHERE deleted_at IS NULL AND latitude IS NOT NULL;
|
||||||
|
CREATE INDEX idx_complexes_active ON complexes(is_active) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 complex_aliases — 楼盘别名
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘(别名随楼盘级联删除) |
|
||||||
|
| alias | VARCHAR(200) | NOT NULL | 别名(最多20字/条,多别名多行存储) |
|
||||||
|
| is_system | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=系统/标准别名(只读),FALSE=用户自定义 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_aliases_complex ON complex_aliases(complex_id);
|
||||||
|
CREATE INDEX idx_complex_aliases_alias_trgm ON complex_aliases USING gin(alias gin_trgm_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 complex_business_areas — 楼盘与商圈多对多
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| business_area_id | UUID | NOT NULL, FK→business_areas, CASCADE | 关联商圈 |
|
||||||
|
| is_primary | BOOLEAN | NOT NULL DEFAULT FALSE | 主商圈(唯一)用于列表显示 |
|
||||||
|
| PRIMARY KEY | (complex_id, business_area_id) | | 联合主键(楼盘与商圈多对多) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 主商圈只能有一个
|
||||||
|
CREATE UNIQUE INDEX idx_complex_biz_area_primary ON complex_business_areas(complex_id) WHERE is_primary = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 complex_schools — 楼盘与学校关联
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| school_id | UUID | NOT NULL, FK→schools, CASCADE | 对口学校 |
|
||||||
|
| zone_type | VARCHAR(30) | | 学区类型:`guaranteed`=对口 / `reference`=参考 / `lottery`=摇号 |
|
||||||
|
| PRIMARY KEY | (complex_id, school_id) | | 联合主键(楼盘与学校多对多) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_schools_school ON complex_schools(school_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:删除此关联记录时,需同步清理对应房源的学区标注(Application 层事务处理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 complex_metro_stations — 楼盘与地铁站关联
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| station_id | UUID | NOT NULL, FK→metro_stations, CASCADE | 关联地铁站 |
|
||||||
|
| distance_meters | INTEGER | | 步行距离(米) |
|
||||||
|
| PRIMARY KEY | (complex_id, station_id) | | 联合主键(楼盘与地铁站多对多) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_metro_complex ON complex_metro_stations(complex_id);
|
||||||
|
CREATE INDEX idx_complex_metro_station ON complex_metro_stations(station_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.11 buildings — 楼栋/单元
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 楼栋名,如「1号楼」「A栋2单元」 |
|
||||||
|
| is_standard | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=标准结构(经运营核准) |
|
||||||
|
| property_usage_type | VARCHAR(20) | | 物业类型(可与楼盘不同,如商住楼盘内有纯商铺楼栋) |
|
||||||
|
| built_year | SMALLINT | | 竣工年份 |
|
||||||
|
| total_floors | SMALLINT | | 总层数 |
|
||||||
|
| land_use_years | VARCHAR(30) | | 土地使用年限 |
|
||||||
|
| has_elevator | BOOLEAN | | 是否有电梯 |
|
||||||
|
| school_id | UUID | FK→schools, SET NULL | 关联对口学校(楼栋级别的学区差异) |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(楼栋被删除或合并) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_buildings_complex ON buildings(complex_id) WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_buildings_name ON buildings(complex_id, name) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.12 room_units — 房号/结构单元(楼层+房间号)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| building_id | UUID | NOT NULL, FK→buildings, CASCADE | 所属楼栋 |
|
||||||
|
| floor | SMALLINT | NOT NULL | 楼层(实际层数,地下为负数) |
|
||||||
|
| floor_name | VARCHAR(20) | | 楼层名称展示,如「1层」「B1层」 |
|
||||||
|
| room_no | VARCHAR(30) | NOT NULL | 房号,如「01」「101」 |
|
||||||
|
| display_no | VARCHAR(50) | | 展示用完整房号,如「3-1-101」 |
|
||||||
|
| is_standard | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=已归一化为标准结构 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已拆除/不存在 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_room_units_building ON room_units(building_id) WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_room_units_unique ON room_units(building_id, floor, room_no) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.13 complex_photos — 楼盘照片
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| category | VARCHAR(20) | NOT NULL | `complex`=楼盘图 / `layout`=户型图 / `vr`=VR全景 / `other`=其他 |
|
||||||
|
| file_key | TEXT | NOT NULL | R2/S3 路径 |
|
||||||
|
| thumbnail_key | TEXT | | 缩略图路径 |
|
||||||
|
| file_name | VARCHAR(255) | | 原始文件名 |
|
||||||
|
| file_size | INTEGER | | 文件大小(bytes) |
|
||||||
|
| width | INTEGER | | 图片宽度(px) |
|
||||||
|
| height | INTEGER | | 图片高度(px) |
|
||||||
|
| is_cover | BOOLEAN | NOT NULL DEFAULT FALSE | 楼盘封面图 |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | 同类别内的排序顺序 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 上传人(操作员工) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_photos_complex ON complex_photos(complex_id);
|
||||||
|
CREATE INDEX idx_complex_photos_category ON complex_photos(complex_id, category);
|
||||||
|
CREATE UNIQUE INDEX idx_complex_photos_cover ON complex_photos(complex_id) WHERE is_cover = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.14 complex_attachments — 楼盘附件
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| file_key | TEXT | NOT NULL | R2/S3 存储路径 |
|
||||||
|
| file_name | VARCHAR(255) | NOT NULL | 原始文件名 |
|
||||||
|
| file_size | INTEGER | | 文件大小(bytes) |
|
||||||
|
| file_type | VARCHAR(50) | | MIME type |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | 同类别内的排序顺序 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | 上传人(操作员工) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.15 complex_price_trends — 楼盘价格走势(月度)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | 所属楼盘 |
|
||||||
|
| record_month | DATE | NOT NULL | 月份(统一存为该月1日,如 2026-04-01) |
|
||||||
|
| avg_sale_price | NUMERIC(12,2) | | 月均售价(万元/套) |
|
||||||
|
| avg_unit_price | NUMERIC(10,2) | | 月均单价(元/m²) |
|
||||||
|
| transaction_count | INTEGER | | 成交套数 |
|
||||||
|
| listing_count | INTEGER | | 当月挂牌套数 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_complex_price_trend_month ON complex_price_trends(complex_id, record_month);
|
||||||
|
CREATE INDEX idx_complex_price_trend_complex ON complex_price_trends(complex_id, record_month DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、枚举常量
|
||||||
|
|
||||||
|
### complexes.building_type(建筑类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
slab = 板楼
|
||||||
|
tower = 塔楼
|
||||||
|
slab_tower = 板塔结合
|
||||||
|
```
|
||||||
|
|
||||||
|
### complexes.water_type / electricity_type
|
||||||
|
|
||||||
|
```
|
||||||
|
civil = 民水/民电(住宅水电费率)
|
||||||
|
commercial = 商水/商电(商业水电费率,费用较高,影响买家决策)
|
||||||
|
```
|
||||||
|
|
||||||
|
### complex_schools.zone_type(学区类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
guaranteed = 对口(直升)
|
||||||
|
reference = 参考(可能入读)
|
||||||
|
lottery = 摇号(通过摇号入学)
|
||||||
|
```
|
||||||
|
|
||||||
|
### buildings.is_standard / room_units.is_standard
|
||||||
|
|
||||||
|
```
|
||||||
|
TRUE = 已标准化(楼栋/房号已经运营核准,可用于精准房源定位)
|
||||||
|
FALSE = 非标(用户自输入,未核准,存在歧义风险)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、查询模式
|
||||||
|
|
||||||
|
### 5.1 楼盘名称联想搜索(录入房源时的自动补全)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 使用全文检索向量,支持中文分词近似匹配
|
||||||
|
SELECT id, name, address_summary, district_id
|
||||||
|
FROM complexes
|
||||||
|
WHERE search_vector @@ plainto_tsquery('simple', :keyword)
|
||||||
|
OR name ILIKE :keyword_prefix -- 前缀精确匹配优先
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND is_active = TRUE
|
||||||
|
ORDER BY
|
||||||
|
ts_rank(search_vector, plainto_tsquery('simple', :keyword)) DESC,
|
||||||
|
name
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 楼盘列表(含房源数量统计)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
c.id, c.name, c.address, c.latitude, c.longitude,
|
||||||
|
d.name AS district_name,
|
||||||
|
ba.name AS primary_business_area,
|
||||||
|
COUNT(DISTINCT b.id) AS building_count,
|
||||||
|
COUNT(DISTINCT p.id) FILTER (WHERE p.status IN ('for_sale','for_sale_rent')) AS sale_count,
|
||||||
|
COUNT(DISTINCT p.id) FILTER (WHERE p.status IN ('for_rent','for_sale_rent')) AS rent_count
|
||||||
|
FROM complexes c
|
||||||
|
LEFT JOIN districts d ON d.id = c.district_id
|
||||||
|
LEFT JOIN complex_business_areas cba ON cba.complex_id = c.id AND cba.is_primary = TRUE
|
||||||
|
LEFT JOIN business_areas ba ON ba.id = cba.business_area_id
|
||||||
|
LEFT JOIN buildings b ON b.complex_id = c.id AND b.is_active = TRUE
|
||||||
|
LEFT JOIN properties p ON p.complex_id = c.id AND p.deleted_at IS NULL
|
||||||
|
WHERE c.deleted_at IS NULL
|
||||||
|
AND c.district_id = ANY(:district_ids) -- 区域筛选
|
||||||
|
GROUP BY c.id, d.name, ba.name
|
||||||
|
ORDER BY c.name
|
||||||
|
LIMIT 20 OFFSET :offset;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 查询楼盘下的楼层-房号矩阵(结构管理)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 选中单元后,加载楼层×房号矩阵
|
||||||
|
SELECT
|
||||||
|
ru.floor,
|
||||||
|
ru.floor_name,
|
||||||
|
ru.room_no,
|
||||||
|
ru.display_no,
|
||||||
|
ru.is_standard,
|
||||||
|
p.id AS property_id, -- 如果该房号已有房源,关联显示
|
||||||
|
p.status AS property_status
|
||||||
|
FROM room_units ru
|
||||||
|
LEFT JOIN properties p ON p.building_id = ru.building_id
|
||||||
|
AND p.room_no = ru.room_no
|
||||||
|
AND p.floor = ru.floor
|
||||||
|
AND p.deleted_at IS NULL
|
||||||
|
WHERE ru.building_id = :building_id
|
||||||
|
AND ru.is_active = TRUE
|
||||||
|
ORDER BY ru.floor DESC, ru.room_no;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、触发器
|
||||||
|
|
||||||
|
### 6.1 楼盘全文检索向量(含别名)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION update_complex_search_vector()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.address_summary, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_complex_search_vector
|
||||||
|
BEFORE INSERT OR UPDATE OF name, address_summary, address
|
||||||
|
ON complexes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
|
||||||
|
|
||||||
|
-- 别名变更时同步更新楼盘 search_vector
|
||||||
|
CREATE OR REPLACE FUNCTION update_complex_search_on_alias()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE complexes
|
||||||
|
SET search_vector = (
|
||||||
|
setweight(to_tsvector('simple', COALESCE(name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple',
|
||||||
|
COALESCE((SELECT string_agg(alias, ' ') FROM complex_aliases WHERE complex_id = complexes.id), '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(address_summary, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(address, '')), 'D')
|
||||||
|
),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = COALESCE(NEW.complex_id, OLD.complex_id);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_complex_alias_search
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON complex_aliases
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_on_alias();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、禁止操作
|
||||||
|
|
||||||
|
- ❌ **严禁直接修改 complexes.name**:楼盘名称变更必须走「楼盘合并」流程或「管理员申请」,通过 Application 层拦截任何直接 UPDATE `name` 字段的操作
|
||||||
|
- ❌ **严禁硬删除 complexes 记录**:有房源关联的楼盘不可删除(`RESTRICT` 外键),已有房源的楼盘软删除后房源仍可正常访问
|
||||||
|
- ❌ **严禁删除 complex_schools 关联而不清理房源学区标注**:必须在同一事务中清理对应 `property.school_ids` 数据
|
||||||
|
- ❌ **严禁在楼盘坐标为 NULL 时将其用于地图聚合**:坐标为空时不参与地图展示,过滤条件:`WHERE latitude IS NOT NULL`
|
||||||
|
- ❌ **严禁在 lock_info=TRUE 时绕过 Application 层直接修改楼盘信息字段**:锁定状态必须在服务层检查,不依赖数据库约束
|
||||||
|
- ❌ **严禁在没有 deleted_at IS NULL 过滤的情况下查询 complexes**:楼盘软删除过滤必须存在
|
||||||
526
Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
# Fonrey — 登录与账号认证数据模型(DATA_MODEL_LOGIN)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/accounts/` — 账号认证、登录安全、密码管理
|
||||||
|
> **关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v2.0)
|
||||||
|
> **关联技术方案**: `Project/fonrey/TECH_STACK/登录管理技术方案.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **UserAccount(用户账号)**:系统登录主体,必须与员工档案(`org.Staff`)1:1 绑定。分为 Tenant Admin(超级管理账号,每租户唯一,username 固定为联系人手机号)和普通员工账号(username 固定为员工手机号)。
|
||||||
|
- **LoginAttempt(登录尝试记录)**:记录每次登录行为(成功/失败),用于安全审计和账号锁定判断,保留 ≥ 90 天。
|
||||||
|
- **PasswordResetToken(密码重置令牌)**:~~通过邮件找回密码时生成的一次性令牌~~ 已废弃,详见 `SmsOtpRecord`。
|
||||||
|
- **SmsOtpRecord(短信验证码记录)**:找回密码和手机验证码登录时向用户手机号发送的 6 位一次性验证码。用 `scene` 字段区分场景(`password_reset` / `login`),有效期按场景不同(找回密码 10 分钟,验证码登录 5 分钟);找回密码验证通过后颁发 `sms_reset_token`(有效期 15 分钟),验证码登录验证通过后直接颁发 Session Token;使用后立即作废。
|
||||||
|
- **PasswordHistory(历史密码记录)**:保存最近 3 次密码哈希,用于防止重复使用历史密码。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **账号与员工强绑定**:每个登录账号 **必须** 与 `org.Staff` 中的员工档案 1:1 绑定(Tenant Admin 例外,可不绑定)。
|
||||||
|
2. **用户名规则统一为手机号**:
|
||||||
|
- Tenant Admin:**固定为该租户联系人的手机号**(11 位数字),来源于 `public.tenants.contact_phone`,全局唯一,创建后不可更改
|
||||||
|
- 普通员工:**固定为员工手机号**(11 位数字),同租户内唯一,创建后不可变更
|
||||||
|
3. **初始密码强制修改**:新账号及密码重置后,`is_initial_password = True`,首次登录必须修改密码,不可跳过。
|
||||||
|
4. **账号锁定机制**:同一账号连续密码错误 ≥ 5 次,状态置为 `locked`,30 分钟后自动恢复;Tenant Admin 可提前手动解锁。
|
||||||
|
5. **员工离职联动**:员工离职时,对应账号的 `status` 自动置为 `disabled`,不可登录,历史操作记录保留。
|
||||||
|
6. **不支持自助注册**:所有账号由有权限的管理角色创建,普通员工账号在新增员工时由系统自动生成。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
UserAccount
|
||||||
|
│
|
||||||
|
├── 1:1 ── org.Staff (实名绑定,普通员工必须)
|
||||||
|
├── 1:N ── LoginAttempt (登录审计记录)
|
||||||
|
├── 1:N ── SmsOtpRecord (短信验证码记录,找回密码用)
|
||||||
|
├── 1:N ── PasswordHistory (历史密码记录)
|
||||||
|
└── M:1 ── UserAccount.created_by (创建人自引用)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema 归属
|
||||||
|
|
||||||
|
| 表 | Schema 位置 | 说明 |
|
||||||
|
|----|------------|------|
|
||||||
|
| `user_accounts` | 租户 Schema | 账号数据按租户隔离,username 唯一性在 Schema 维度生效 |
|
||||||
|
| `login_attempts` | 租户 Schema | 审计记录属于租户,跨租户不可见 |
|
||||||
|
| `sms_otp_records` | 租户 Schema | 短信验证码记录,找回密码用 |
|
||||||
|
| `password_histories` | 租户 Schema | 历史密码与账号绑定 |
|
||||||
|
|
||||||
|
> **注意**:Tenant Code 验证相关逻辑在 **Public Schema**(`shared_apps`),使用 `django-tenants` 的 `TenantModel`,不在本文档范围内,详见 `DATA_MODEL.md` §四(公共 Schema)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 `user_accounts` — 账号主表(租户 Schema)
|
||||||
|
|
||||||
|
**表说明**:系统登录主体,每个租户内独立隔离,`username` 唯一性约束在 Schema 维度生效。
|
||||||
|
|
||||||
|
#### 字段定义
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|
||||||
|
|--------|------|------|--------|------|
|
||||||
|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键(审计场景下 BigInt 更直观;跨环境引用使用 UUID 扩展字段见下) |
|
||||||
|
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 登录名;普通员工为手机号(11 位数字);Tenant Admin 为自定义字符串;创建后不可更改 |
|
||||||
|
| `password` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希存储,使用 Django `make_password` |
|
||||||
|
| `email` | `VARCHAR(254)` | `NULL` | `NULL` | 绑定邮箱;完全可选,在本系统无任何必须业务用途;若填写则在同租户内唯一 |
|
||||||
|
| `phone_enc` | `TEXT` | `NULL` | `NULL` | 手机号 AES-256-GCM 加密密文(`core.encryption`);普通员工必填 |
|
||||||
|
| `phone_hash` | `VARCHAR(64)` | `NULL` | `NULL` | 手机号 SHA-256 哈希;用于唯一性校验和查询;不可反推原文 |
|
||||||
|
| `staff_id` | `BIGINT` | `FK → org_staff.id`, `NULL`, `UNIQUE` | `NULL` | 员工档案绑定(1:1);普通员工必须有值;Tenant Admin 可为空 |
|
||||||
|
| `is_tenant_admin` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否为该租户的超级管理账号;每个租户最多 1 个(应用层约束) |
|
||||||
|
| `status` | `VARCHAR(10)` | `NOT NULL`, `CHECK(status IN ('active','disabled','locked'))` | `'active'` | 账号状态;`locked` 为密码错误锁定,30 分钟自动恢复 |
|
||||||
|
| `is_initial_password` | `BOOLEAN` | `NOT NULL` | `TRUE` | 初始密码标记;True 时登录成功后强制跳转修改密码页,不可跳过 |
|
||||||
|
| `last_login` | `TIMESTAMPTZ` | `NULL` | `NULL` | 最后登录时间 |
|
||||||
|
| `locked_until` | `TIMESTAMPTZ` | `NULL` | `NULL` | 锁定到期时间;到期后应用层将 status 恢复 active |
|
||||||
|
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 账号创建时间 |
|
||||||
|
| `updated_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 最后更新时间(触发器维护) |
|
||||||
|
| `created_by` | `BIGINT` | `FK → user_accounts.id`, `NULL` | `NULL` | 创建人;普通员工由 Tenant Admin 创建;Tenant Admin 由平台运营创建(可为 NULL) |
|
||||||
|
|
||||||
|
#### 唯一性约束
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UNIQUE (username) -- Schema 内唯一,跨租户不冲突(django-tenants 机制保障)
|
||||||
|
UNIQUE (email) -- 同租户内邮箱唯一(可为 NULL,NULL 不参与唯一性校验)
|
||||||
|
UNIQUE (phone_hash) -- 同租户内手机号唯一(通过 hash 实现,不暴露原文)
|
||||||
|
UNIQUE (staff_id) -- 员工档案 1:1 绑定
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 索引
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX uq_user_accounts_username ON user_accounts (username);
|
||||||
|
CREATE UNIQUE INDEX uq_user_accounts_email ON user_accounts (email) WHERE email IS NOT NULL;
|
||||||
|
CREATE UNIQUE INDEX uq_user_accounts_phone ON user_accounts (phone_hash) WHERE phone_hash IS NOT NULL;
|
||||||
|
CREATE INDEX idx_user_accounts_status ON user_accounts (status);
|
||||||
|
CREATE INDEX idx_user_accounts_staff ON user_accounts (staff_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Django Model 定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
# apps/accounts/models.py
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccountManager(BaseUserManager):
|
||||||
|
def create_user(self, username, password, **extra_fields):
|
||||||
|
if not username:
|
||||||
|
raise ValueError("username 不能为空")
|
||||||
|
user = self.model(username=username, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccount(AbstractBaseUser):
|
||||||
|
"""
|
||||||
|
租户级用户账号。
|
||||||
|
- 普通员工:username 固定为员工手机号(11 位数字)
|
||||||
|
- Tenant Admin:username 固定为该租户联系人手机号(来源于 public.tenants.contact_phone)
|
||||||
|
注意:此表位于租户 Schema,username 唯一性约束在 Schema 维度生效。
|
||||||
|
"""
|
||||||
|
username = models.CharField(max_length=30)
|
||||||
|
email = models.EmailField(null=True, blank=True)
|
||||||
|
phone_enc = models.TextField(null=True, blank=True) # AES-256-GCM 加密密文
|
||||||
|
phone_hash = models.CharField(max_length=64, null=True, blank=True) # SHA-256 哈希索引
|
||||||
|
staff = models.OneToOneField(
|
||||||
|
'org.Staff',
|
||||||
|
null=True, blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='account',
|
||||||
|
)
|
||||||
|
is_tenant_admin = models.BooleanField(default=False)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('active', 'Active'), ('disabled', 'Disabled'), ('locked', 'Locked')],
|
||||||
|
default='active',
|
||||||
|
)
|
||||||
|
is_initial_password = models.BooleanField(default=True)
|
||||||
|
last_login = models.DateTimeField(null=True, blank=True)
|
||||||
|
locked_until = models.DateTimeField(null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
'self',
|
||||||
|
null=True, blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='created_accounts',
|
||||||
|
)
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'username'
|
||||||
|
REQUIRED_FIELDS = []
|
||||||
|
|
||||||
|
objects = UserAccountManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'user_accounts'
|
||||||
|
# Schema 内唯一约束
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['username'], name='uq_user_accounts_username'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.username} ({'admin' if self.is_tenant_admin else 'staff'})"
|
||||||
|
|
||||||
|
def is_locked(self) -> bool:
|
||||||
|
"""检查账号是否处于锁定状态(含自动过期判断)"""
|
||||||
|
from django.utils import timezone
|
||||||
|
if self.status == 'locked':
|
||||||
|
if self.locked_until and timezone.now() >= self.locked_until:
|
||||||
|
# 锁定已到期,应用层自动恢复(实际由 service 层处理)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 `login_attempts` — 登录尝试审计表(租户 Schema)
|
||||||
|
|
||||||
|
**表说明**:记录每次登录请求(成功/失败),用于安全审计和锁定判断。数据保留 ≥ 90 天,不得提前清理。
|
||||||
|
|
||||||
|
#### 字段定义
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|
||||||
|
|--------|------|------|--------|------|
|
||||||
|
| `id` | `BIGSERIAL` | `NOT NULL` | — | 自增主键(与 attempted_at 组成复合 PK) |
|
||||||
|
| `attempted_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 尝试时间(分区键,必须在复合主键中) |
|
||||||
|
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 尝试登录的用户名(冗余存储,即使账号不存在也记录) |
|
||||||
|
| `ip_address` | `INET` | `NOT NULL` | — | 来源 IP 地址(支持 IPv4/IPv6) |
|
||||||
|
| `user_agent` | `TEXT` | `NULL` | `NULL` | 客户端 User-Agent(Electron 版本信息) |
|
||||||
|
| `success` | `BOOLEAN` | `NOT NULL` | — | 是否登录成功 |
|
||||||
|
| `failure_reason` | `VARCHAR(30)` | `NULL` | `NULL` | 失败原因;可选值见下方枚举 |
|
||||||
|
|
||||||
|
> ⚠️ **分区说明**:`login_attempts` 为高写入审计表,采用 `PARTITION BY RANGE (attempted_at)` 按月分区。主键为 `(id, attempted_at)` 复合主键(分区表规范:主键必须包含分区键)。
|
||||||
|
|
||||||
|
**DDL**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE login_attempts (
|
||||||
|
id BIGSERIAL NOT NULL,
|
||||||
|
attempted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键
|
||||||
|
username VARCHAR(30) NOT NULL,
|
||||||
|
ip_address INET NOT NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
success BOOLEAN NOT NULL,
|
||||||
|
failure_reason VARCHAR(30)
|
||||||
|
CHECK (failure_reason IS NULL OR failure_reason IN (
|
||||||
|
'wrong_password','wrong_captcha','account_locked',
|
||||||
|
'account_disabled','tenant_not_found',
|
||||||
|
'wrong_otp','otp_expired'
|
||||||
|
)),
|
||||||
|
PRIMARY KEY (id, attempted_at) -- 分区表主键必须包含分区键
|
||||||
|
) PARTITION BY RANGE (attempted_at);
|
||||||
|
|
||||||
|
CREATE TABLE login_attempts_2026_04 PARTITION OF login_attempts
|
||||||
|
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||||
|
CREATE TABLE login_attempts_2026_05 PARTITION OF login_attempts
|
||||||
|
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||||
|
CREATE TABLE login_attempts_default PARTITION OF login_attempts DEFAULT;
|
||||||
|
```
|
||||||
|
|
||||||
|
**`failure_reason` 枚举值**:
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| `wrong_password` | 用户名或密码错误 |
|
||||||
|
| `wrong_captcha` | 行为验证码失败 |
|
||||||
|
| `account_locked` | 账号已锁定 |
|
||||||
|
| `account_disabled` | 账号已停用 |
|
||||||
|
| `tenant_not_found` | 租户不存在(理论上不应出现,防御性记录) |
|
||||||
|
| `wrong_otp` | 短信验证码错误 |
|
||||||
|
| `otp_expired` | 短信验证码已过期 |
|
||||||
|
|
||||||
|
#### 索引
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_login_attempts_username ON login_attempts (username);
|
||||||
|
CREATE INDEX idx_login_attempts_ip ON login_attempts (ip_address);
|
||||||
|
CREATE INDEX idx_login_attempts_time ON login_attempts (attempted_at DESC);
|
||||||
|
-- 复合索引:按账号查询最近失败记录(锁定判断场景)
|
||||||
|
CREATE INDEX idx_login_attempts_fail_check ON login_attempts (username, success, attempted_at DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Django Model 定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LoginAttempt(models.Model):
|
||||||
|
"""
|
||||||
|
登录尝试审计记录。
|
||||||
|
- 合规保留周期:≥ 90 天
|
||||||
|
- 注意:failure_reason 不得存储密码明文(含错误密码)
|
||||||
|
"""
|
||||||
|
FAILURE_REASONS = [
|
||||||
|
('wrong_password', '用户名或密码错误'),
|
||||||
|
('wrong_captcha', '行为验证码失败'),
|
||||||
|
('account_locked', '账号已锁定'),
|
||||||
|
('account_disabled', '账号已停用'),
|
||||||
|
('tenant_not_found', '租户不存在'),
|
||||||
|
('wrong_otp', '短信验证码错误'),
|
||||||
|
('otp_expired', '短信验证码已过期'),
|
||||||
|
]
|
||||||
|
|
||||||
|
username = models.CharField(max_length=30)
|
||||||
|
ip_address = models.GenericIPAddressField()
|
||||||
|
user_agent = models.TextField(null=True, blank=True)
|
||||||
|
success = models.BooleanField()
|
||||||
|
failure_reason = models.CharField(max_length=30, null=True, blank=True, choices=FAILURE_REASONS)
|
||||||
|
attempted_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'login_attempts'
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['username']),
|
||||||
|
models.Index(fields=['ip_address']),
|
||||||
|
models.Index(fields=['-attempted_at']),
|
||||||
|
models.Index(fields=['username', 'success', '-attempted_at'],
|
||||||
|
name='idx_login_attempts_fail_check'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.username} @ {self.attempted_at} - {'OK' if self.success else self.failure_reason}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 `sms_otp_records` — 短信验证码记录表(租户 Schema)
|
||||||
|
|
||||||
|
**表说明**:记录找回密码和手机验证码登录两个场景中,向用户手机号发送的短信验证码及其状态。用 `scene` 字段区分场景(`password_reset` / `login`),有效期和发送频率上限按场景独立控制。原 `password_reset_tokens`(邮件令牌)已废弃,由本表替代。
|
||||||
|
|
||||||
|
#### 字段定义
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|
||||||
|
|--------|------|------|--------|------|
|
||||||
|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
|
||||||
|
| `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 |
|
||||||
|
| `phone_hash` | `VARCHAR(64)` | `NOT NULL` | — | 收码手机号 SHA-256 哈希(与账号的 `phone_hash` 一致,冗余存储便于限频查询) |
|
||||||
|
| `scene` | `VARCHAR(20)` | `NOT NULL` | — | 使用场景:`password_reset`(找回密码)或 `login`(验证码登录) |
|
||||||
|
| `otp_hash` | `VARCHAR(128)` | `NOT NULL` | — | 验证码 PBKDF2 哈希(禁止明文存储,服务端校验时重新哈希比对) |
|
||||||
|
| `expires_at` | `TIMESTAMPTZ` | `NOT NULL` | — | 过期时间(`password_reset`:`created_at + 10 分钟`;`login`:`created_at + 5 分钟`) |
|
||||||
|
| `is_used` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否已验证通过;通过后立即置 True,防止重放攻击 |
|
||||||
|
| `verify_attempts` | `SMALLINT` | `NOT NULL` | `0` | 已尝试验证次数;≥ 5 次后本条记录强制作废(`is_used = TRUE`) |
|
||||||
|
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 创建时间 |
|
||||||
|
|
||||||
|
#### 索引
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_sms_otp_user ON sms_otp_records (user_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_sms_otp_phone ON sms_otp_records (phone_hash, created_at DESC);
|
||||||
|
CREATE INDEX idx_sms_otp_active ON sms_otp_records (user_id, expires_at) WHERE is_used = FALSE;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Django Model 定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SmsOtpRecord(models.Model):
|
||||||
|
"""
|
||||||
|
短信验证码记录(找回密码 + 验证码登录共用)。
|
||||||
|
安全约束:
|
||||||
|
- OTP 明文仅在发送短信时生成,不持久化;仅存 PBKDF2 哈希
|
||||||
|
- 单条记录验证次数 ≥ 5 次后强制作废(is_used=True)
|
||||||
|
- 同一手机号 1 小时内按 scene 独立限频(password_reset ≤ 5 次,login ≤ 10 次)
|
||||||
|
"""
|
||||||
|
SCENE_CHOICES = [
|
||||||
|
('password_reset', '找回密码'),
|
||||||
|
('login', '验证码登录'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='sms_otp_records')
|
||||||
|
phone_hash = models.CharField(max_length=64) # SHA-256 哈希,不暴露手机号原文
|
||||||
|
scene = models.CharField(max_length=20, choices=SCENE_CHOICES) # 区分场景
|
||||||
|
otp_hash = models.CharField(max_length=128) # PBKDF2 哈希,禁止明文存储
|
||||||
|
expires_at = models.DateTimeField() # password_reset: +10min; login: +5min
|
||||||
|
is_used = models.BooleanField(default=False)
|
||||||
|
verify_attempts = models.SmallIntegerField(default=0)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'sms_otp_records'
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['user', '-created_at']),
|
||||||
|
models.Index(fields=['phone_hash', '-created_at']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
from django.utils import timezone
|
||||||
|
return not self.is_used and self.verify_attempts < 5 and timezone.now() < self.expires_at
|
||||||
|
|
||||||
|
def mark_used(self):
|
||||||
|
self.is_used = True
|
||||||
|
self.save(update_fields=['is_used'])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 `password_histories` — 历史密码记录表(租户 Schema)
|
||||||
|
|
||||||
|
**表说明**:保存账号最近 3 次密码哈希,用于防止重复使用历史密码(含初始密码)。
|
||||||
|
|
||||||
|
#### 字段定义
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|
||||||
|
|--------|------|------|--------|------|
|
||||||
|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
|
||||||
|
| `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 |
|
||||||
|
| `password_hash` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希值 |
|
||||||
|
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 记录时间(密码修改时间) |
|
||||||
|
|
||||||
|
#### 索引
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_password_histories_user ON password_histories (user_id, created_at DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Django Model 定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PasswordHistory(models.Model):
|
||||||
|
"""
|
||||||
|
历史密码记录,每个账号保留最近 N 条(默认 3 条)。
|
||||||
|
新密码不得与最近 3 条历史记录相同(含系统初始密码 Fonrey@2025)。
|
||||||
|
"""
|
||||||
|
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='password_histories')
|
||||||
|
password_hash = models.CharField(max_length=128)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'password_histories'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['user', '-created_at']),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Redis 缓存结构(辅助状态,非持久化)
|
||||||
|
|
||||||
|
以下 Redis Key 不存入 PostgreSQL,属于运行时状态,需与数据库状态保持最终一致:
|
||||||
|
|
||||||
|
| Key 格式 | 类型 | TTL | 说明 |
|
||||||
|
|----------|------|-----|------|
|
||||||
|
| `captcha_token:{uuid}` | STRING | 3 分钟 | 滑块验证会话 Token;验证通过后生成 `captcha_pass_token` |
|
||||||
|
| `captcha_pass:{uuid}` | STRING | 3 分钟 | 一次性通过凭证;登录提交时校验后立即删除 |
|
||||||
|
| `login_fail:{tenant_id}:{username}` | STRING(计数) | 30 分钟 | 连续密码错误次数;≥ 5 触发锁定;TTL 30 分钟自动清零 |
|
||||||
|
| `sms_limit:password_reset:{phone}` | STRING(计数) | 1 小时 | 找回密码场景:同一手机号短信发送次数;上限 **5 次**/小时 |
|
||||||
|
| `sms_limit:login:{phone}` | STRING(计数) | 1 小时 | 验证码登录场景:同一手机号短信发送次数;上限 **10 次**/小时 |
|
||||||
|
| `sms_reset_token:{token}` | STRING(user_id) | 15 分钟 | 验证码通过后颁发的一次性重置凭证;用于步骤三提交新密码;使用后立即删除 |
|
||||||
|
| `tenant_verify_ip:{ip}` | STRING(计数) | 1 分钟 | Tenant 验证接口 IP 限流;≥ 10 次拒绝请求 |
|
||||||
|
|
||||||
|
> **一致性说明**:账号锁定状态由 `user_accounts.status` 持久化,Redis 仅做计数触发器。当 Redis 数据丢失(如 Redis 重启),应用层通过 `locked_until` 字段恢复锁定状态判断。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、账号创建流程与状态机
|
||||||
|
|
||||||
|
### 5.1 账号状态机
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 账号生命周期状态机 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
|
||||||
|
[创建账号]
|
||||||
|
│ is_initial_password=True, status=active
|
||||||
|
▼
|
||||||
|
[初始密码态] ─── 使用初始密码登录成功 ───► [强制修改密码页]
|
||||||
|
│ │ 修改成功
|
||||||
|
│ ▼
|
||||||
|
│ [正常使用态]
|
||||||
|
│ status=active
|
||||||
|
│ is_initial_password=False
|
||||||
|
│
|
||||||
|
├── 密码错误 ≥ 5 次 ──────────────────► [锁定态]
|
||||||
|
│ status=locked
|
||||||
|
│ locked_until = now+30min
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────┤
|
||||||
|
│ │ 30分钟到期 │ 管理员手动解锁
|
||||||
|
│ ▼ ▼
|
||||||
|
│ [正常使用态] ◄─── [管理员操作]
|
||||||
|
│
|
||||||
|
└── 员工离职 / 管理员停用 ──► [停用态]
|
||||||
|
status=disabled
|
||||||
|
│
|
||||||
|
员工复职/管理员恢复
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[正常使用态]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 账号创建触发时机
|
||||||
|
|
||||||
|
| 账号类型 | 触发时机 | 创建者 | username 规则 | 初始密码 |
|
||||||
|
|----------|----------|--------|--------------|---------|
|
||||||
|
| Tenant Admin | 平台运营在系统后台开通租户时 | 平台运营 | **固定为该租户联系人手机号**(11 位数字,来源于 `public.tenants.contact_phone`) | **系统统一固定初始密码**(如 `Fonrey@2025`,由平台部署配置设定) |
|
||||||
|
| 普通员工 | Tenant Admin 在「新增员工」时系统自动生成 | 系统(Tenant Admin 触发) | 固定为员工手机号(11 位) | 系统统一初始密码(部署配置) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、关联约束与数据完整性
|
||||||
|
|
||||||
|
### 6.1 与 `org.Staff` 的关联规则
|
||||||
|
|
||||||
|
```
|
||||||
|
org_staff (1) ──── (0..1) user_accounts
|
||||||
|
```
|
||||||
|
|
||||||
|
- 普通员工账号:`staff_id` **必须**有值,且在 `org.Staff` 中对应记录的 `status` 为 active
|
||||||
|
- Tenant Admin:`staff_id` **可为空**(平台运营账号可不绑定实名档案)
|
||||||
|
- 员工离职时(`org.Staff.status` → `resigned`),触发账号 `status` → `disabled`(由 `org` App Service 层调用 `accounts` 服务执行,避免循环依赖)
|
||||||
|
- 账号删除:**不允许物理删除**,仅允许 `status=disabled`,审计记录永久保留
|
||||||
|
|
||||||
|
### 6.2 跨 App 依赖方向
|
||||||
|
|
||||||
|
```
|
||||||
|
accounts ──► org (单向依赖:accounts.UserAccount.staff_id → org.Staff)
|
||||||
|
org ──► accounts (反向触发,通过 Service 层调用,不通过 FK 反查)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **设计原则**:避免循环 FK 依赖,跨 App 的状态联动通过 Service 层的显式调用完成,不在 Model 层建立反向 FK。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、迁移说明(Django Migrations)
|
||||||
|
|
||||||
|
### 初始迁移顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
0001_initial_user_accounts.py # UserAccount 表(依赖 org.Staff 表已存在)
|
||||||
|
0002_login_attempts.py # LoginAttempt 表
|
||||||
|
0003_sms_otp_records.py # SmsOtpRecord 表(替代原 password_reset_tokens)
|
||||||
|
0004_password_histories.py # PasswordHistory 表
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `accounts` App 的迁移依赖 `org` App(`org.Staff` 表须先创建),需在 `INSTALLED_APPS` 中确保 `org` 在 `accounts` 之前
|
||||||
|
- 所有迁移均在**租户 Schema** 下执行(`django-tenants` 的 `migrate_schemas` 命令)
|
||||||
|
- 不得为 `email` 字段设置 `NOT NULL` 约束(允许为空,是否绑定邮箱属于用户选择)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、设计决策说明(ADR)
|
||||||
|
|
||||||
|
| 决策 | 选择 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 主键类型 | `BIGSERIAL` (BigInt) | 登录审计场景下 BigInt 主键更简洁高效;跨环境引用场景少,无需 UUID 的随机性 |
|
||||||
|
| `phone` 字段拆分为 `phone_enc` + `phone_hash` | 是 | 与 `org.Staff` 保持一致;`phone_enc` 保存原文用于展示,`phone_hash` 用于唯一性校验和快速查询,避免加密字段全表扫描 |
|
||||||
|
| 不扩展 Django `User` | 使用 `AbstractBaseUser` | 避免 `django.contrib.auth.User` 的全局唯一性限制(多租户下同一 username 在不同租户是允许的) |
|
||||||
|
| `LoginAttempt` 不设外键到 `UserAccount` | 是(冗余存储 username) | 即使账号被删除(停用),审计记录仍需保留;使用 username 字符串字段保证审计完整性 |
|
||||||
|
| 找回密码机制 | 短信验证码(`SmsOtpRecord`) | 经纪人普遍无邮箱;手机号是本系统唯一已知联系方式,短信核验更直接;废弃邮件找回路径,减少外部依赖(SMTP) |
|
||||||
|
| 历史密码单独建表 | `PasswordHistory` 独立表 | 而非在 `UserAccount` 中存 JSON 数组,便于查询和维护,支持未来扩展保留次数 |
|
||||||
|
| 锁定到期时间持久化 | `locked_until` 字段 | Redis 可能重启丢失数据,持久化 `locked_until` 保证 Redis 故障时锁定状态不丢失 |
|
||||||
348
Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||||
|
# Fonrey — 组织人事数据模型(DATA_MODEL_ORG)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/org/` — 组织架构、员工档案、人事异动、账号体系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **OrgUnit(组织节点)**:公司组织树的节点,类型涵盖事业部 / 大区 / 区域 / 片区 / 门店 / 店组 / 职能。所有业务数据(房源、客源)最终归属到门店或店组级节点。
|
||||||
|
- **Staff(员工)**:系统的核心操作人员,与 Django `auth_user` 绑定登录账号,与 `org_units` 绑定岗位归属。员工的组织归属直接影响数据可见范围。
|
||||||
|
- **StaffTransferLog(人事异动记录)**:记录员工从入职到离职的全生命周期状态变化。每次异动(入职/调动/离职/复职)自动生成一条不可删除的日志。
|
||||||
|
- **StaffAccount(账号信息)**:员工的多平台登录账号体系,包括 Fonrey 主账号 / 58安居客 / 中国网络经纪人等。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **组织层级约束**:店组级部门 **必须** 挂在门店下;经纪人/店管的所属部门 **只能** 是门店或店组。
|
||||||
|
2. **经纪人定义**:职务类别为「置业顾问」的员工即为经纪人,受业务规则特殊约束。
|
||||||
|
3. **人员异动强制日志**:入职、调动、离职、复职等操作均自动生成 `staff_transfer_logs` 记录,不可删除。
|
||||||
|
4. **账号与员工联动**:员工离职后,对应的 `auth_user.is_active` 设为 `False`,不可登录;复职后由管理员手动恢复。
|
||||||
|
5. **手机号敏感字段**:员工手机号 AES-256-GCM 加密存储,SHA-256 哈希用于唯一性校验,通讯录展示脱敏格式。
|
||||||
|
6. **数据归属继承**:员工调动时,名下房源/客源默认跟随员工到新部门;离职时可选择转移给指定账号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
OrgUnit (树形自引用,物化路径)
|
||||||
|
│
|
||||||
|
├── 1:N ── Staff (员工归属一个部门)
|
||||||
|
│ │
|
||||||
|
│ ├── 1:1 ── auth_user (Django 登录账号)
|
||||||
|
│ ├── 1:N ── StaffTransferLog (人事异动记录)
|
||||||
|
│ ├── 1:N ── StaffRewardPunish (奖惩记录)
|
||||||
|
│ ├── 1:N ── StaffAccount (第三方账号绑定)
|
||||||
|
│ └── 1:N ── StaffRemark (管理员备注)
|
||||||
|
│
|
||||||
|
└── 1:1 ── OrgUnit.parent_id (自引用)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 org_units — 组织节点表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 部门/组织名称 |
|
||||||
|
| type | VARCHAR(20) | NOT NULL, CHECK | 枚举:`company` / `division`(事业部) / `region`(大区) / `area`(区域) / `district`(片区) / `store`(门店) / `group`(店组) / `functional`(职能) |
|
||||||
|
| parent_id | UUID | FK→self, RESTRICT | 父节点,根节点为 NULL |
|
||||||
|
| path | TEXT | NOT NULL | 物化路径:`/root_id/.../self_id/`,用于子树查询 |
|
||||||
|
| depth | SMALLINT | NOT NULL DEFAULT 0 | 节点深度(根=0),最大支持 8 层 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 同级排序 |
|
||||||
|
| attribute | VARCHAR(10) | | 直营/加盟,枚举:`direct` / `franchise` |
|
||||||
|
| address_city | VARCHAR(50) | | 部门所在城市 |
|
||||||
|
| address_district | VARCHAR(50) | | 部门所在县区 |
|
||||||
|
| address_detail | VARCHAR(200) | | 详细地址 |
|
||||||
|
| latitude | NUMERIC(10,7) | | 坐标(部门定位针) |
|
||||||
|
| longitude | NUMERIC(10,7) | | 坐标 |
|
||||||
|
| manager_id | UUID | FK→staff.id, SET NULL | 部门负责人(循环依赖,Application 层维护) |
|
||||||
|
| established_at | DATE | | 成立时间 |
|
||||||
|
| phone | VARCHAR(30) | | 部门联系电话 |
|
||||||
|
| ext_start | INTEGER | | 分机号范围:起始 |
|
||||||
|
| ext_end | INTEGER | | 分机号范围:结束 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE = 已关闭部门,仍可在筛选中显示 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;NULL=未删除,非NULL=已软删除 |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_org_units_parent ON org_units(parent_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_org_units_path_prefix ON org_units(path text_pattern_ops); -- 路径前缀查询
|
||||||
|
CREATE INDEX idx_org_units_type ON org_units(type) WHERE deleted_at IS NULL AND is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:
|
||||||
|
- 查询某部门及所有下级:`WHERE path LIKE '/root_id/{target_id}/%'`
|
||||||
|
- 店组(`group`)的 `parent_id` 必须指向一个 `store` 节点,新增前需校验
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 staff — 员工表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| org_unit_id | UUID | NOT NULL, FK→org_units | 当前所属组织节点(门店或店组) |
|
||||||
|
| user_id | INTEGER | UNIQUE, FK→auth_user | Django auth 登录账号 ID |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 真实姓名 |
|
||||||
|
| nickname | VARCHAR(50) | | 昵称(通讯录/显示名) |
|
||||||
|
| employee_no | VARCHAR(30) | UNIQUE | 员工工号,系统自动生成或手动录入 |
|
||||||
|
| role | VARCHAR(30) | NOT NULL, CHECK | 系统角色枚举:`agent`(经纪人) / `store_manager` / `area_manager` / `admin` / `operator` / `system` |
|
||||||
|
| job_title | VARCHAR(100) | | 职务名称,如「高级业务员」 |
|
||||||
|
| job_category | VARCHAR(50) | | 职务类别,如「置业顾问」(经纪人判定字段) |
|
||||||
|
| job_level | SMALLINT | | 职级(数字) |
|
||||||
|
| supervisor_id | UUID | FK→staff.id, SET NULL | 直属上级 |
|
||||||
|
| status | VARCHAR(20) | NOT NULL DEFAULT 'active' | `active`(在职) / `probation`(试用) / `resigned`(离职) / `frozen`(冻结) |
|
||||||
|
| phone_enc | BYTEA | | AES-256-GCM 加密手机号 |
|
||||||
|
| phone_hash | VARCHAR(64) | | SHA-256 哈希,用于唯一性索引 |
|
||||||
|
| phone_hide | BOOLEAN | NOT NULL DEFAULT FALSE | 通讯录是否隐藏手机号 |
|
||||||
|
| email | VARCHAR(255) | | 邮箱 |
|
||||||
|
| extension | VARCHAR(20) | | 分机号 |
|
||||||
|
| avatar_key | TEXT | | R2/S3 头像路径 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE 时账号不可登录(联动 auth_user.is_active) |
|
||||||
|
| is_system_admin | BOOLEAN | NOT NULL DEFAULT FALSE | 是否为系统管理员(影响权限上限) |
|
||||||
|
| first_joined_at | DATE | | 首次入职日期(计算工龄起点) |
|
||||||
|
| rejoined_at | DATE | | 最近复职日期 |
|
||||||
|
| resigned_at | DATE | | 最近离职日期 |
|
||||||
|
| joined_count | SMALLINT | NOT NULL DEFAULT 1 | 累计入职次数 |
|
||||||
|
| industry_exp_years | SMALLINT | | 行业经验(年) |
|
||||||
|
| mentor_id | UUID | FK→staff.id, SET NULL | 师傅(带教员工) |
|
||||||
|
| business_type | VARCHAR(50) | | 业务类型 |
|
||||||
|
| bank_name | VARCHAR(100) | | 银行名称 |
|
||||||
|
| bank_account | VARCHAR(50) | | 银行卡号(内部财务用) |
|
||||||
|
| partner_no | VARCHAR(50) | | 联号 |
|
||||||
|
| recruit_by_id | UUID | FK→staff.id, SET NULL | 招聘人 |
|
||||||
|
| recruit_source | VARCHAR(50) | | 招聘来源 |
|
||||||
|
| referrer_id | UUID | FK→staff.id, SET NULL | 转介人 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;离职员工不可硬删除,保留档案 |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_staff_employee_no ON staff(employee_no) WHERE deleted_at IS NULL;
|
||||||
|
CREATE UNIQUE INDEX idx_staff_phone_hash ON staff(phone_hash) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_staff_org_unit ON staff(org_unit_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_staff_supervisor ON staff(supervisor_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_staff_status ON staff(status) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:
|
||||||
|
- `is_active = FALSE` 时对应 `auth_user.is_active` 同步设为 False,通过 Django signal 实现
|
||||||
|
- 离职员工(`status = 'resigned'`)不可硬删除,保留档案以便房源/客源历史关联查询
|
||||||
|
- 经纪人判定:`job_category = '置业顾问'`,部分权限逻辑基于此字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 staff_personal_info — 员工个人信息扩展表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| staff_id | UUID | UNIQUE, NOT NULL, FK→staff | 1:1 关系 |
|
||||||
|
| gender | VARCHAR(10) | | `male` / `female` / `unknown` |
|
||||||
|
| id_type | VARCHAR(20) | | 证件类型:`id_card`(身份证) / `passport` / `other` |
|
||||||
|
| id_number_enc | BYTEA | | 证件号码(AES 加密) |
|
||||||
|
| id_number_hash | VARCHAR(64) | | SHA-256 哈希(实名认证比对用) |
|
||||||
|
| id_verified | BOOLEAN | NOT NULL DEFAULT FALSE | 是否实名认证通过 |
|
||||||
|
| id_verified_at | TIMESTAMPTZ | | 认证时间 |
|
||||||
|
| birthdate | DATE | | 出生日期 |
|
||||||
|
| native_place | VARCHAR(100) | | 籍贯 |
|
||||||
|
| domicile_type | VARCHAR(20) | | 户籍性质 |
|
||||||
|
| marital_status | VARCHAR(20) | | 婚姻状况 |
|
||||||
|
| political_status | VARCHAR(20) | | 政治面貌 |
|
||||||
|
| has_children | BOOLEAN | | 有无子女 |
|
||||||
|
| education_level | VARCHAR(20) | | 最高学历 |
|
||||||
|
| ethnicity | VARCHAR(20) | | 民族 |
|
||||||
|
| domicile_address | VARCHAR(200) | | 户口所在地 |
|
||||||
|
| residence_address | VARCHAR(200) | | 居住地址 |
|
||||||
|
| work_start_date | DATE | | 参加工作时间 |
|
||||||
|
| emergency_contact | VARCHAR(50) | | 紧急联系人 |
|
||||||
|
| emergency_phone_enc | BYTEA | | 紧急联系人电话(加密) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| updated_by | UUID | FK→staff.id, SET NULL | 最后修改人(操作员工) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 staff_transfer_logs — 人事异动记录
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff, RESTRICT | 被操作员工 |
|
||||||
|
| transfer_type | VARCHAR(30) | NOT NULL, CHECK | 枚举见下方 |
|
||||||
|
| old_value | JSONB | | 变动前的值快照,格式:`{"field": "org_unit_id", "value": "...", "label": "门店A"}` |
|
||||||
|
| new_value | JSONB | | 变动后的值快照 |
|
||||||
|
| transfer_date | DATE | NOT NULL | 异动生效日期(可以是过去日期) |
|
||||||
|
| remarks | VARCHAR(50) | | 备注(最多50字) |
|
||||||
|
| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | 操作人(必填,异动审计必须记录) |
|
||||||
|
| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 系统操作时间 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动,等同 operated_at) |
|
||||||
|
| ⚠️ 无 deleted_at | — | — | 异动记录**不可删除** |
|
||||||
|
|
||||||
|
**transfer_type 枚举**:
|
||||||
|
```
|
||||||
|
onboard = 入职
|
||||||
|
transfer = 调动(含平调/晋升/降职)
|
||||||
|
resign = 离职
|
||||||
|
rejoin = 复职
|
||||||
|
supervisor_change = 上级变动
|
||||||
|
role_change = 角色变更
|
||||||
|
freeze = 账号冻结
|
||||||
|
unfreeze = 账号恢复
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_transfer_logs_staff ON staff_transfer_logs(staff_id, transfer_date DESC);
|
||||||
|
CREATE INDEX idx_transfer_logs_type ON staff_transfer_logs(transfer_type, operated_at DESC);
|
||||||
|
CREATE INDEX idx_transfer_logs_operator ON staff_transfer_logs(operator_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 staff_reward_punish — 奖惩记录
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff | 被奖惩员工 |
|
||||||
|
| rp_date | DATE | NOT NULL | 奖惩日期 |
|
||||||
|
| category | VARCHAR(50) | NOT NULL | 奖惩类别(枚举由 lookup 表维护) |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 奖惩名称(与类别联动) |
|
||||||
|
| remarks | TEXT | | 备注 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
| created_by | UUID | FK→staff.id, SET NULL | 录入人(操作员工) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录最后更新时间(系统自动) |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除时间戳;NULL=未删除,非NULL=已软删除 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 staff_work_experiences / staff_educations / staff_trainings / staff_family_members
|
||||||
|
|
||||||
|
这四张表结构类似,均为 1:N 附属于 `staff`,存储员工档案中「工作经历」「教育经历」「培训经历」「家庭主要成员」信息。详见下方汇总:
|
||||||
|
|
||||||
|
| 表名 | 关键字段 |
|
||||||
|
|------|---------|
|
||||||
|
| `staff_work_experiences` | staff_id, company, job_title, start_date, end_date, reason, reference_name, reference_phone |
|
||||||
|
| `staff_educations` | staff_id, stage, school, major, start_date, end_date, enrollment_status, degree |
|
||||||
|
| `staff_trainings` | staff_id, training_name, training_date, certificate |
|
||||||
|
| `staff_family_members` | staff_id, relation(称谓), name, birthdate, occupation, work_unit, phone_enc |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 staff_accounts — 员工第三方账号绑定
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | 主键(系统生成,业务无关) |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff | 所属员工(必填,证件信息随员工关联) |
|
||||||
|
| platform | VARCHAR(30) | NOT NULL, CHECK | `fonrey`(主账号) / `58anjuke` / `cnreic`(中国网络经纪人) / `wechat_mp`(微信公众号) |
|
||||||
|
| account_no | VARCHAR(100) | | 账号/手机号 |
|
||||||
|
| is_real_name_match | BOOLEAN | | 实名信息一致性(中国网络经纪人专用) |
|
||||||
|
| is_bound | BOOLEAN | NOT NULL DEFAULT FALSE | 是否已绑定 |
|
||||||
|
| bound_at | TIMESTAMPTZ | | 绑定时间 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录创建时间(系统自动) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、枚举常量
|
||||||
|
|
||||||
|
### Staff.role(系统角色)
|
||||||
|
|
||||||
|
| 值 | 含义 | 数据可见范围默认 |
|
||||||
|
|----|------|----------------|
|
||||||
|
| `agent` | 一线经纪人 | 本人/本组 |
|
||||||
|
| `store_manager` | 店长 | 本门店 |
|
||||||
|
| `area_manager` | 区域经理 | 本区域 |
|
||||||
|
| `admin` | 系统管理员 | 全公司 |
|
||||||
|
| `operator` | 运营/行政 | 全公司(只读为主) |
|
||||||
|
| `system` | 系统账号(定时任务用) | — |
|
||||||
|
|
||||||
|
### Staff.status(员工状态)
|
||||||
|
|
||||||
|
```
|
||||||
|
active = 正式在职
|
||||||
|
probation = 试用期
|
||||||
|
resigned = 已离职(不可删除,保留档案)
|
||||||
|
frozen = 账号冻结(在职但无法登录)
|
||||||
|
```
|
||||||
|
|
||||||
|
### OrgUnit.type(组织类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
company = 公司根节点(每个租户唯一)
|
||||||
|
division = 事业部
|
||||||
|
region = 大区
|
||||||
|
area = 区域
|
||||||
|
district = 片区
|
||||||
|
store = 门店(经纪人最小归属单位)
|
||||||
|
group = 店组(门店下的业务小组)
|
||||||
|
functional = 职能部门(行政/财务等)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、查询模式
|
||||||
|
|
||||||
|
### 5.1 查询某部门及所有下级的在职员工
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 利用物化路径高效查询子树
|
||||||
|
SELECT s.*
|
||||||
|
FROM staff s
|
||||||
|
JOIN org_units ou ON s.org_unit_id = ou.id
|
||||||
|
WHERE ou.path LIKE '/root_id/{target_org_unit_id}/%'
|
||||||
|
OR ou.id = '{target_org_unit_id}'
|
||||||
|
AND s.deleted_at IS NULL
|
||||||
|
AND s.status != 'resigned';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 查询员工完整异动历史
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT stl.*,
|
||||||
|
s.name as operator_name,
|
||||||
|
ou.name as operator_org
|
||||||
|
FROM staff_transfer_logs stl
|
||||||
|
JOIN staff s ON stl.operator_id = s.id
|
||||||
|
JOIN org_units ou ON s.org_unit_id = ou.id
|
||||||
|
WHERE stl.staff_id = :staff_id
|
||||||
|
ORDER BY stl.transfer_date DESC, stl.operated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 获取员工的直接上下级链
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 直属上级
|
||||||
|
SELECT supervisor.* FROM staff
|
||||||
|
JOIN staff supervisor ON staff.supervisor_id = supervisor.id
|
||||||
|
WHERE staff.id = :staff_id AND supervisor.deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、禁止操作
|
||||||
|
|
||||||
|
- ❌ **严禁硬删除 staff 记录**:离职员工需通过 `deleted_at + status = 'resigned'` 软删除,历史房源/跟进日志依赖 `staff.id` 外键
|
||||||
|
- ❌ **严禁删除 staff_transfer_logs**:异动记录为不可变审计日志
|
||||||
|
- ❌ **严禁直接修改 staff.user_id**:账号绑定关系变更需走专门的账号管理流程
|
||||||
|
- ❌ **严禁绕过组织层级约束**:店组不在门店下的数据操作需在 Application 层校验并拒绝
|
||||||
|
- ❌ **严禁明文存储员工手机号和证件号**:必须走 `EncryptedPhoneField` / `EncryptedIDField`
|
||||||
1377
Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md
Normal file
1165
Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md
Normal file
1106
Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md
Normal file
397
Project/fonrey/DATA_MODEL/DATA_MODEL_SETTING.md
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||||
|
|
||||||
|
# Fonrey — 系统配置模块数据模型(DATA_MODEL_SETTING)
|
||||||
|
|
||||||
|
> **定位**:本文件是 `apps/setting/` 模块的数据模型权威来源。
|
||||||
|
> **版本**:v1.0 | **日期**:2026-04-27
|
||||||
|
> **关联 PRD**:`PRD/系统配置/系统配置模块PRD.md`
|
||||||
|
> **关联文档**:`DATA_MODEL/ENUMS.md`、`DATA_MODEL/DATA_MODEL.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、模块定位与架构边界
|
||||||
|
|
||||||
|
### 1.1 系统配置模块职责
|
||||||
|
|
||||||
|
系统配置模块(`apps/setting/`)负责管理三类性质不同的配置数据:
|
||||||
|
|
||||||
|
| 类型 | 说明 | 表 | Schema |
|
||||||
|
| -------------- | ---------------- | --------------------------------------------- | -------------- |
|
||||||
|
| **A. 固定系统枚举** | 平台级固定值域,所有租户共享 | `enum_labels` | Public(shared) |
|
||||||
|
| **B. 可配置枚举** | 各租户选项不同,管理员可增删排序 | `lookup_groups` + `lookup_items` | Tenant |
|
||||||
|
| **C. 行为规则与开关** | 标量配置开关 + 字段必填规则 | `tenant_settings` + `field_requirement_rules` | Tenant |
|
||||||
|
|
||||||
|
> **重要区分**:
|
||||||
|
> - 类型 A (`enum_labels`) 已在 `DATA_MODEL/ENUMS.md` 完整定义,**本文件不重复**
|
||||||
|
> - 类型 B/C 均存于 **租户 Schema**,由租户管理员通过界面维护
|
||||||
|
> - `apps/setting/` 是 `tenant_apps`(**非** `shared_apps`)
|
||||||
|
|
||||||
|
### 1.2 依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/setting/
|
||||||
|
├── 依赖 → core.cache(Redis,统一租户前缀)
|
||||||
|
├── 依赖 → org.Staff(created_by / updated_by FK)
|
||||||
|
└── 被依赖 ← apps/property(读取字段规则、枚举选项)
|
||||||
|
← apps/client(读取查重范围、枚举选项)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、可配置枚举表(类型 B)
|
||||||
|
|
||||||
|
### 2.1 `lookup_groups`(枚举分组)
|
||||||
|
|
||||||
|
每个分组代表一类可配置枚举(如「客源来源」「跟进目的」),由研发预置,租户管理员**不可新增或删除分组**,仅可管理分组内的选项。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ============================================================
|
||||||
|
-- 可配置枚举分组(Tenant Schema)
|
||||||
|
-- 研发预置,租户不可修改分组本身
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE lookup_groups (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关)
|
||||||
|
module VARCHAR(50) NOT NULL, -- 'client' | 'property'
|
||||||
|
key VARCHAR(100) NOT NULL, -- 'source' | 'follow_purpose'
|
||||||
|
label_zh VARCHAR(50) NOT NULL, -- 界面显示名称,如「客源来源」
|
||||||
|
description TEXT, -- 说明文案(前端 tooltip 使用)
|
||||||
|
sort_order SMALLINT NOT NULL DEFAULT 0, -- 排序权重(数值越小越靠前)
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动)
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动)
|
||||||
|
UNIQUE (module, key)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**MVP 预置分组(种子数据)**:
|
||||||
|
|
||||||
|
| module | key | label_zh | description |
|
||||||
|
|--------|-----|----------|-------------|
|
||||||
|
| `client` | `source` | 客源来源 | 客源从何处获取,用于来源渠道分析 |
|
||||||
|
| `client` | `follow_purpose` | 跟进目的 | 客源跟进时选择的目的分类 |
|
||||||
|
| `property` | `source` | 房源来源 | 房源从何处获取 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 `lookup_items`(枚举选项)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ============================================================
|
||||||
|
-- 可配置枚举选项(Tenant Schema)
|
||||||
|
-- 租户管理员可增删排序;is_system=True 的预制项不可物理删除
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE lookup_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关)
|
||||||
|
group_id UUID NOT NULL REFERENCES lookup_groups(id) ON DELETE CASCADE, -- 所属枚举分组(关联 lookup_groups)
|
||||||
|
value VARCHAR(100) NOT NULL, -- 存储值,英文 snake_case(如 'door_to_door')
|
||||||
|
label_zh VARCHAR(50) NOT NULL, -- 显示文本(如「上门」)
|
||||||
|
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- True=系统预制,不可删除,仅可停用
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用;FALSE 后前端下拉不展示,历史数据保留并追加「(已停用)」后缀
|
||||||
|
sort_order SMALLINT NOT NULL DEFAULT 0, -- 排序权重(数值越小越靠前)
|
||||||
|
created_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 系统预制时为 NULL
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录创建时间(系统自动)
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动)
|
||||||
|
UNIQUE (group_id, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lookup_items_group_active
|
||||||
|
ON lookup_items(group_id, is_active, sort_order);
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键约束**:
|
||||||
|
- `is_system = TRUE` 的记录不允许物理删除(Service 层强制拦截)
|
||||||
|
- `is_active = FALSE` 后:前端录入下拉不展示;历史已选该值的记录保留原值,展示时追加「(已停用)」后缀
|
||||||
|
- `value` 一旦写入不允许修改(历史数据依赖);如需改名,停用旧项、新增新项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 MVP 预置种子数据(`is_system = TRUE`)
|
||||||
|
|
||||||
|
以下选项在租户初始化时自动写入:
|
||||||
|
|
||||||
|
#### 客源来源(`client.source`)
|
||||||
|
|
||||||
|
| value | label_zh | sort_order |
|
||||||
|
|-------|----------|------------|
|
||||||
|
| `store_reception` | 门店接待 | 1 |
|
||||||
|
| `old_client_referral` | 老客户转介绍 | 2 |
|
||||||
|
| `stationed_dispatch` | 驻守派单 | 3 |
|
||||||
|
| `walk_in` | 上门 | 4 |
|
||||||
|
| `online_58` | 网络-58同城 | 5 |
|
||||||
|
| `online_anjuke` | 网络-安居客 | 6 |
|
||||||
|
| `wechat` | 微信 | 7 |
|
||||||
|
| `friend_referral` | 朋友介绍 | 8 |
|
||||||
|
|
||||||
|
#### 跟进目的(`client.follow_purpose`)
|
||||||
|
|
||||||
|
| value | label_zh | sort_order |
|
||||||
|
|-------|----------|------------|
|
||||||
|
| `callback` | 回拨 | 1 |
|
||||||
|
| `push_property` | 推房 | 2 |
|
||||||
|
| `showing` | 带看 | 3 |
|
||||||
|
| `maintain` | 维护 | 4 |
|
||||||
|
| `other` | 其他 | 5 |
|
||||||
|
|
||||||
|
#### 房源来源(`property.source`)
|
||||||
|
|
||||||
|
| value | label_zh | sort_order |
|
||||||
|
|-------|----------|------------|
|
||||||
|
| `proactive_development` | 主动开发 | 1 |
|
||||||
|
| `owner_walk_in` | 业主上门 | 2 |
|
||||||
|
| `old_client_referral` | 老客户转介绍 | 3 |
|
||||||
|
| `online_inquiry` | 网络来电 | 4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、行为规则与开关(类型 C)
|
||||||
|
|
||||||
|
### 3.1 `tenant_settings`(标量配置键值表)
|
||||||
|
|
||||||
|
存储开关(bool)、阈值(int)、单选枚举(string)等标量类型配置项。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ============================================================
|
||||||
|
-- 租户标量配置(键值对)(Tenant Schema)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE tenant_settings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关)
|
||||||
|
category VARCHAR(50) NOT NULL, -- 配置分类:'client' | 'property' | 'showroom'
|
||||||
|
key VARCHAR(100) NOT NULL, -- 配置 key,如 'duplicate_check_scope'
|
||||||
|
value JSONB NOT NULL, -- 存储任意类型(bool/int/str),如 {"v": "self"}
|
||||||
|
value_type VARCHAR(20) NOT NULL -- 'bool' | 'int' | 'string' | 'enum'(用于前端渲染控件)
|
||||||
|
CHECK (value_type IN ('bool', 'int', 'string', 'enum')),
|
||||||
|
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 最后修改人(关联 staff 表)
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动)
|
||||||
|
UNIQUE (category, key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_tenant_settings_category ON tenant_settings(category);
|
||||||
|
```
|
||||||
|
|
||||||
|
**存储格式约定**:
|
||||||
|
- `bool`:`{"v": true}` 或 `{"v": false}`
|
||||||
|
- `int`:`{"v": 30}`
|
||||||
|
- `string`:`{"v": "some_value"}`
|
||||||
|
- `enum`:`{"v": "self", "choices": ["self", "dept", "company"]}` — `choices` 由代码硬编码,不存 DB
|
||||||
|
|
||||||
|
**MVP 阶段预置 key**:
|
||||||
|
|
||||||
|
| category | key | value_type | 默认值 | 说明 |
|
||||||
|
|----------|-----|-----------|--------|------|
|
||||||
|
| `client` | `duplicate_check_scope` | `enum` | `{"v": "self"}` | 新增私客查重范围:`self`(本人)/ `dept`(本部门)/ `company`(全公司) |
|
||||||
|
|
||||||
|
> 未来 P1 阶段可按需追加 key,无需修改表结构。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 `field_requirement_rules`(字段必填规则表)
|
||||||
|
|
||||||
|
按「模块 × 房源用途 × 交易状态 × 字段」四元组确定一条规则,控制录入界面的字段显示状态。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ============================================================
|
||||||
|
-- 字段必填/隐藏规则(Tenant Schema)
|
||||||
|
-- MVP 仅支持 module='property'
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE field_requirement_rules (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 主键(系统生成,业务无关)
|
||||||
|
module VARCHAR(20) NOT NULL, -- 'property' | 'client'(MVP 只用 'property')
|
||||||
|
entity_type VARCHAR(50) NOT NULL, -- 与 property.property_type CHECK 约束值对齐
|
||||||
|
-- 'residential'|'villa'|'commercial_residential'|'shop'|'office'|'other'
|
||||||
|
trade_status VARCHAR(20) NOT NULL, -- 交易大类:'sale'|'rent'|'sale_rent'|'*'(全部)
|
||||||
|
CHECK (trade_status IN ('sale', 'rent', 'sale_rent', '*')),
|
||||||
|
field_key VARCHAR(50) NOT NULL, -- 字段 key,如 'orientation'|'decoration'|'floor'
|
||||||
|
requirement VARCHAR(10) NOT NULL -- 规则值
|
||||||
|
CHECK (requirement IN ('required', 'optional', 'hidden')),
|
||||||
|
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 最后修改人(关联 staff 表)
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 记录最后更新时间(系统自动)
|
||||||
|
UNIQUE (module, entity_type, trade_status, field_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_field_req_lookup
|
||||||
|
ON field_requirement_rules(module, entity_type, trade_status);
|
||||||
|
```
|
||||||
|
|
||||||
|
**与 `property.property_type` 对齐说明**:
|
||||||
|
|
||||||
|
`entity_type` 的值域与 `property.property_type` 的 CHECK 约束完全一致:
|
||||||
|
|
||||||
|
| entity_type | property_type label_zh |
|
||||||
|
|-------------|----------------------|
|
||||||
|
| `residential` | 住宅 |
|
||||||
|
| `villa` | 别墅 |
|
||||||
|
| `commercial_residential` | 商住 |
|
||||||
|
| `shop` | 商铺 |
|
||||||
|
| `office` | 写字楼 |
|
||||||
|
| `other` | 其他 |
|
||||||
|
|
||||||
|
**`trade_status` 与 `property.status` 的映射关系**:
|
||||||
|
|
||||||
|
| trade_status | 对应 property.status 值 |
|
||||||
|
|--------------|------------------------|
|
||||||
|
| `sale` | `for_sale` |
|
||||||
|
| `rent` | `for_rent` |
|
||||||
|
| `sale_rent` | `for_sale_rent` |
|
||||||
|
| `*` | 所有状态通用规则(fallback) |
|
||||||
|
|
||||||
|
> **重要**:`trade_status` 是录入场景的交易意图分类,不是 `property.status` 的完整枚举。规则匹配逻辑:先查精确匹配(`entity_type + trade_status`),不存在则查 `*` 通配规则。
|
||||||
|
|
||||||
|
**MVP 初始规则(研发预置,管理员可覆盖)**:
|
||||||
|
|
||||||
|
| module | entity_type | trade_status | field_key | requirement |
|
||||||
|
|--------|-------------|--------------|-----------|-------------|
|
||||||
|
| `property` | `residential` | `sale` | `orientation` | `optional` |
|
||||||
|
| `property` | `residential` | `sale` | `decoration` | `optional` |
|
||||||
|
| `property` | `residential` | `sale` | `floor` | `optional` |
|
||||||
|
| `property` | `residential` | `sale` | `building_area` | `required` |
|
||||||
|
| `property` | `residential` | `sale` | `inner_area` | `optional` |
|
||||||
|
| `property` | `residential` | `sale` | `room_layout` | `required` |
|
||||||
|
| `property` | `residential` | `rent` | `decoration` | `optional` |
|
||||||
|
| `property` | `residential` | `rent` | `floor` | `optional` |
|
||||||
|
| `property` | `residential` | `rent` | `building_area` | `required` |
|
||||||
|
| `property` | `residential` | `rent` | `room_layout` | `required` |
|
||||||
|
|
||||||
|
**MVP 可配置字段范围(对应 PRD AC-4)**:
|
||||||
|
|
||||||
|
| field_key | 说明 | 字段类型 |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `orientation` | 朝向(`property.orientation` 枚举) | 枚举 |
|
||||||
|
| `decoration` | 装修情况(`property.decoration` 枚举) | 枚举 |
|
||||||
|
| `floor` | 所在楼层/总楼层 | 数值 |
|
||||||
|
| `building_area` | 建筑面积(㎡) | 数值 |
|
||||||
|
| `inner_area` | 套内面积(㎡) | 数值 |
|
||||||
|
| `room_layout` | 房型(室/厅/卫) | 数值组 |
|
||||||
|
| `ownership_years` | 产权年限(年) | 数值 |
|
||||||
|
| `parking_count` | 车位数 | 数值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、服务层设计
|
||||||
|
|
||||||
|
所有业务模块**禁止直接查询配置表**,必须通过统一服务层读取:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# apps/setting/services/tenant_settings_service.py
|
||||||
|
|
||||||
|
class TenantSettingsService:
|
||||||
|
"""
|
||||||
|
系统配置统一读取服务。
|
||||||
|
所有配置均经 Redis 缓存,TTL 5min,写入时主动 invalidate。
|
||||||
|
Redis Key 规范:{tenant_schema}:setting:{type}:{key}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, category: str, key: str, default=None):
|
||||||
|
"""
|
||||||
|
读取标量配置(tenant_settings 表)
|
||||||
|
Cache Key: {tenant_schema}:setting:kv:{category}.{key}
|
||||||
|
返回 JSONB value 字段中 'v' 的值(已解包)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set(self, category: str, key: str, value, updated_by_id) -> None:
|
||||||
|
"""
|
||||||
|
写入标量配置 + 主动 invalidate 缓存
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_lookup_items(self, module: str, key: str) -> list[dict]:
|
||||||
|
"""
|
||||||
|
获取可配置枚举选项(lookup_items 表)
|
||||||
|
仅返回 is_active=True 的项,按 sort_order 排序
|
||||||
|
Cache Key: {tenant_schema}:setting:lookup:{module}.{key}
|
||||||
|
返回格式:[{"value": "walk_in", "label_zh": "上门", "is_system": True}, ...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_field_requirements(
|
||||||
|
self, module: str, entity_type: str, trade_status: str
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
获取字段必填规则
|
||||||
|
匹配顺序:精确匹配(entity_type + trade_status) > 通配规则(trade_status='*')
|
||||||
|
Cache Key: {tenant_schema}:setting:field_req:{module}.{entity_type}.{trade_status}
|
||||||
|
返回格式:{"orientation": "optional", "decoration": "required", ...}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、Redis 缓存键规范
|
||||||
|
|
||||||
|
| 用途 | Cache Key | TTL | 失效触发 |
|
||||||
|
|------|-----------|-----|---------|
|
||||||
|
| 标量配置 | `{schema}:setting:kv:{category}.{key}` | 300s | `TenantSettingsService.set()` |
|
||||||
|
| 可配置枚举 | `{schema}:setting:lookup:{module}.{key}` | 300s | 管理员保存 lookup_items |
|
||||||
|
| 字段规则 | `{schema}:setting:field_req:{module}.{entity_type}.{trade_status}` | 300s | 管理员保存 field_requirement_rules |
|
||||||
|
| 客源规则(整体) | `{schema}:setting:client_rules` | 300s | 任意客源规则变更 |
|
||||||
|
|
||||||
|
> TTL 300s(5 分钟)对应 PRD 成功指标「配置变更生效时延 ≤ 5 分钟」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/setting/
|
||||||
|
├── models/
|
||||||
|
│ ├── lookup.py # LookupGroup, LookupItem
|
||||||
|
│ └── setting.py # TenantSetting, FieldRequirementRule
|
||||||
|
├── services/
|
||||||
|
│ └── tenant_settings_service.py # 统一配置读取服务(禁止直接查表)
|
||||||
|
├── views/
|
||||||
|
│ ├── lookup_views.py # 参数配置页面(US-SETTING-001-A)
|
||||||
|
│ ├── field_rule_views.py # 房源字段规则(US-SETTING-001-B)
|
||||||
|
│ └── client_rule_views.py # 客源规则(US-SETTING-001-C)
|
||||||
|
├── templates/setting/
|
||||||
|
├── fixtures/
|
||||||
|
│ ├── lookup_groups.json # 分组种子数据(3 组)
|
||||||
|
│ ├── lookup_items.json # 选项种子数据(is_system=True)
|
||||||
|
│ ├── tenant_settings.json # 默认配置种子数据
|
||||||
|
│ └── field_requirement_rules.json # 默认字段规则
|
||||||
|
├── migrations/
|
||||||
|
│ ├── 0001_lookup_groups.py
|
||||||
|
│ ├── 0002_lookup_items.py
|
||||||
|
│ ├── 0003_tenant_settings.py
|
||||||
|
│ └── 0004_field_requirement_rules.py
|
||||||
|
└── urls.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、迁移执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
0001_lookup_groups # 先建分组表(无外键依赖)
|
||||||
|
0002_lookup_items # 再建选项表(依赖 lookup_groups + staff)
|
||||||
|
0003_tenant_settings # 独立,无外键依赖
|
||||||
|
0004_field_requirement_rules # 独立,仅依赖 staff
|
||||||
|
```
|
||||||
|
|
||||||
|
迁移执行后,通过 `call_command('loaddata', 'lookup_groups')` 等方式加载 fixtures 种子数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、关键约束与禁止项
|
||||||
|
|
||||||
|
| 约束 | 规则 |
|
||||||
|
|------|------|
|
||||||
|
| 不可删除系统预制项 | `lookup_items.is_system = True` 的记录,Service 层硬拦截物理删除请求 |
|
||||||
|
| 不可修改已有 value | `lookup_items.value` 写入后只读;修改请停用旧项 + 新增新项 |
|
||||||
|
| 不可直接查询配置表 | 业务模块(property/client)**必须**通过 `TenantSettingsService` 读取,禁止 ORM 直查 |
|
||||||
|
| entity_type 值域 | 必须与 `property.property_type` CHECK 约束完全一致(见第三章) |
|
||||||
|
| Redis Key 前缀 | 必须携带租户 schema 前缀,格式:`{tenant_schema}:setting:{type}:{key}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、设计决策(ADR)
|
||||||
|
|
||||||
|
| 决策 | 选择 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 枚举两级架构 | `enum_labels`(Public/固定)+ `lookup_items`(Tenant/可配置)分离 | 保障系统一致性的同时给租户灵活度 |
|
||||||
|
| `lookup_groups` 由研发预置 | 是 | 防止租户随意创建不规范分组,控制可配置范围边界 |
|
||||||
|
| `tenant_settings` 使用 JSONB | 是 | 支持 bool/int/string 等多类型,无需为每类型单独建列 |
|
||||||
|
| `field_requirement_rules` 不新增字段 | 是 | 规则层只控制「必填/选填/隐藏」,字段存在性由 DATA_MODEL_PROPERTY 决定 |
|
||||||
|
| 服务层统一读取 | `TenantSettingsService` | 统一缓存管理,业务层与配置存储解耦 |
|
||||||
|
| trade_status 不复用 property.status | 独立 `sale/rent/sale_rent/*` | 录入场景的交易意图与房源全生命周期状态不同,避免耦合 |
|
||||||
264
Project/fonrey/DATA_MODEL/DATA_MODEL文档要求.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
|
||||||
|
`DATA_MODEL.md` 的核心目标是:**让 AI 在触碰任何数据相关代码时,都能理解业务语义,而不只是看到一堆字段名。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 必须包含的内容
|
||||||
|
|
||||||
|
#### 1. 领域概览(Domain Overview)
|
||||||
|
|
||||||
|
用业务语言(不是技术语言)描述核心概念和它们的关系。这是 AI 理解"为什么"的基础。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
|
||||||
|
## Domain Overview
|
||||||
|
|
||||||
|
This is a **multi-tenant** project management tool.
|
||||||
|
|
||||||
|
Core concepts:
|
||||||
|
- **Workspace**: The top-level container, maps to a paying customer (company)
|
||||||
|
- **Project**: Lives inside a Workspace, has a lifecycle (draft → active → archived)
|
||||||
|
- **Task**: The atomic unit of work, always belongs to a Project
|
||||||
|
- **Member**: A User's role within a specific Workspace (not global)
|
||||||
|
|
||||||
|
Key business rules:
|
||||||
|
- A User can belong to multiple Workspaces with different roles
|
||||||
|
- Deleting a Project soft-deletes all its Tasks (never hard delete)
|
||||||
|
- Only Workspace `owner` or `admin` can invite new Members
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. 实体关系图(Entity Relationship)
|
||||||
|
|
||||||
|
用文字或 ASCII 图表达清楚关系,不要依赖读者去脑补。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Entity Relationships
|
||||||
|
|
||||||
|
Workspace (1) ──── (N) Project
|
||||||
|
Workspace (1) ──── (N) Member
|
||||||
|
Member (N) ──── (1) User ← same User, different roles per Workspace
|
||||||
|
Project (1) ──── (N) Task
|
||||||
|
Task (N) ──── (1) Member ← assignee
|
||||||
|
Task (1) ──── (N) Comment
|
||||||
|
Task (N) ──── (N) Tag ← via task_tags join table
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. 完整 Schema 定义(Schema Definition)
|
||||||
|
|
||||||
|
每个表都需要:字段 + 类型 + 约束 + **业务注释**。注释是给 AI 的关键上下文。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
### workspaces
|
||||||
|
| Column | Type | Constraints | Notes |
|
||||||
|
|--------------|-------------|-------------------|--------------------------------|
|
||||||
|
| id | uuid | PK, default gen | |
|
||||||
|
| slug | text | UNIQUE, NOT NULL | URL identifier, immutable after creation |
|
||||||
|
| name | text | NOT NULL | |
|
||||||
|
| plan | text | NOT NULL | enum: 'free' | 'pro' | 'enterprise' |
|
||||||
|
| created_at | timestamptz | NOT NULL, default now() | |
|
||||||
|
| deleted_at | timestamptz | nullable | soft delete, filter WHERE deleted_at IS NULL |
|
||||||
|
|
||||||
|
### members
|
||||||
|
| Column | Type | Constraints | Notes |
|
||||||
|
|--------------|-------------|-------------------|--------------------------------|
|
||||||
|
| id | uuid | PK | |
|
||||||
|
| workspace_id | uuid | FK → workspaces.id, CASCADE DELETE | |
|
||||||
|
| user_id | uuid | FK → users.id | |
|
||||||
|
| role | text | NOT NULL | enum: 'owner' \| 'admin' \| 'member' \| 'viewer' |
|
||||||
|
| invited_by | uuid | FK → members.id, nullable | null = founding owner |
|
||||||
|
| joined_at | timestamptz | nullable | null = invitation pending |
|
||||||
|
|
||||||
|
-- UNIQUE(workspace_id, user_id) — one membership per workspace per user
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. 枚举与常量(Enums & Constants)
|
||||||
|
|
||||||
|
把所有枚举值集中在一处,AI 就不会自己发明状态值。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Enums & Constants
|
||||||
|
|
||||||
|
### Task Status (ordered, represents workflow progression)
|
||||||
|
pending → in_progress → in_review → done → cancelled
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Only forward transitions are allowed (no reopening cancelled tasks)
|
||||||
|
- `done` and `cancelled` are terminal states
|
||||||
|
- Changing status logs an entry in task_activity
|
||||||
|
|
||||||
|
### Member Role Permissions
|
||||||
|
| Action | owner | admin | member | viewer |
|
||||||
|
|---------------------|-------|-------|--------|--------|
|
||||||
|
| Invite members | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Delete project | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| Create tasks | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Comment on tasks | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. 索引策略(Indexes)
|
||||||
|
|
||||||
|
告诉 AI 哪些查询是热路径,避免它写出全表扫描的代码。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Indexes
|
||||||
|
|
||||||
|
-- tasks is the most queried table, optimize for these patterns:
|
||||||
|
CREATE INDEX idx_tasks_project_status ON tasks(project_id, status) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_tasks_assignee ON tasks(assignee_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_members_user ON members(user_id); -- "get all workspaces for a user"
|
||||||
|
|
||||||
|
-- Avoid: never query tasks without a project_id filter
|
||||||
|
-- Avoid: never do SELECT * on tasks, always select specific columns
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. 软删除约定(Soft Delete Convention)
|
||||||
|
|
||||||
|
如果使用软删除,必须明确说明规则,否则 AI 会写出漏掉过滤条件的查询。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Soft Delete Convention
|
||||||
|
|
||||||
|
Tables with soft delete: workspaces, projects, tasks
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- ALL queries MUST include `WHERE deleted_at IS NULL` unless explicitly retrieving deleted records
|
||||||
|
- Use `deletedAt: new Date()` to soft delete, never use DELETE SQL
|
||||||
|
- Cascading: deleting a Project sets deleted_at on all its Tasks in the same transaction
|
||||||
|
- Deleted records are purged by a cron job after 30 days (hard delete)
|
||||||
|
|
||||||
|
⚠️ AI Note: Never generate a query on these tables without the soft delete filter.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. 多租户隔离规则(Multi-tenancy Rules)
|
||||||
|
|
||||||
|
这是安全的核心,必须让 AI 理解每次查询都要带租户过滤。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Multi-tenancy & Data Isolation
|
||||||
|
|
||||||
|
This is a **workspace-scoped** multi-tenant system.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- EVERY query involving projects, tasks, members MUST be scoped by workspace_id
|
||||||
|
- Never query tasks directly by id alone — always join through project → workspace
|
||||||
|
- The workspace_id must come from the authenticated session, never from user input
|
||||||
|
- Row Level Security (RLS) is enabled on Supabase for all tenant tables
|
||||||
|
|
||||||
|
Correct pattern:
|
||||||
|
SELECT * FROM tasks
|
||||||
|
JOIN projects ON tasks.project_id = projects.id
|
||||||
|
WHERE projects.workspace_id = :workspaceId ← always present
|
||||||
|
AND tasks.id = :taskId
|
||||||
|
|
||||||
|
Wrong pattern (security hole):
|
||||||
|
SELECT * FROM tasks WHERE id = :taskId ← missing tenant scope
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 8. 核心查询模式(Common Query Patterns)
|
||||||
|
|
||||||
|
把最常用的查询写出来,AI 会直接复用,而不是重新发明。
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
````md
|
||||||
|
## Common Query Patterns
|
||||||
|
|
||||||
|
### Get tasks for a project (with assignee info)
|
||||||
|
```sql
|
||||||
|
SELECT t.*, m.user_id, u.name as assignee_name
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN members m ON t.assignee_id = m.id
|
||||||
|
LEFT JOIN users u ON m.user_id = u.id
|
||||||
|
WHERE t.project_id = :projectId
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
ORDER BY t.created_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check if user has permission in workspace
|
||||||
|
```sql
|
||||||
|
SELECT role FROM members
|
||||||
|
WHERE workspace_id = :workspaceId
|
||||||
|
AND user_id = :userId
|
||||||
|
AND joined_at IS NOT NULL; -- pending invitations don't count
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 9. 明确禁止的操作(Forbidden Operations)
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Forbidden Operations
|
||||||
|
|
||||||
|
- ❌ Never hard DELETE on workspaces, projects, or tasks — use soft delete
|
||||||
|
- ❌ Never UPDATE a task's status by skipping intermediate states
|
||||||
|
- ❌ Never expose internal UUIDs in URLs — use slugs for workspaces/projects
|
||||||
|
- ❌ Never store user PII (email, name) in tasks or comments — always join from users table
|
||||||
|
- ❌ Never write a migration that drops a column without a deprecation period
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 10. 数据迁移约定(Migration Convention)
|
||||||
|
|
||||||
|
md
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Migration Convention
|
||||||
|
|
||||||
|
- Migration tool: Drizzle Kit
|
||||||
|
- File naming: `0001_create_workspaces.ts`, sequential, never rename
|
||||||
|
- Every migration must be reversible (include `down` function)
|
||||||
|
- Never edit a migration that has been merged to main
|
||||||
|
- Backfill scripts go in /scripts, not in migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 内容优先级总结
|
||||||
|
|
||||||
|
|优先级|内容|为什么关键|
|
||||||
|
|---|---|---|
|
||||||
|
|🔴 必须|领域概览 + 业务规则|AI 需要理解"为什么"才能做正确取舍|
|
||||||
|
|🔴 必须|完整 Schema + 字段注释|防止字段命名歧义和类型错误|
|
||||||
|
|🔴 必须|软删除 + 多租户规则|安全漏洞和数据污染的高发区|
|
||||||
|
|🟡 重要|枚举常量 + 状态机|防止 AI 自己发明状态值|
|
||||||
|
|🟡 重要|常用查询模式|复用 > 重新发明|
|
||||||
|
|🟢 加分|索引策略|引导 AI 写出性能友好的查询|
|
||||||
|
|🟢 加分|迁移约定|保持 schema 演进的可控性|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**核心原则**:`DATA_MODEL.md` 写的不是给数据库看的 DDL,而是给 AI 看的**业务契约**。每一个"显而易见"的业务规则,在 AI 眼里都是需要被明确告知的约束。没写的,它都会自己猜。
|
||||||
786
Project/fonrey/DATA_MODEL/ENUMS.md
Normal file
@@ -0,0 +1,786 @@
|
|||||||
|
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||||||
|
|
||||||
|
# Fonrey — 统一枚举字典(ENUMS)
|
||||||
|
|
||||||
|
> **定位**:本文件是 Fonrey 全局枚举标准(Public + Tenant)的统一实现基线。
|
||||||
|
> **版本**:v2.2
|
||||||
|
> **日期**:2026-04-28
|
||||||
|
> **适用范围**:`DATA_MODEL_PUBLIC.md`、`DATA_MODEL_LOGIN.md`、`DATA_MODEL_ORG.md`、`DATA_MODEL_COMPLEX.md`、`DATA_MODEL_PROPERTY.md`、`DATA_MODEL_CLIENT.md`、`DATA_MODEL_PERMISSION.md`、`DATA_MODEL_SETTING.md`
|
||||||
|
|
||||||
|
> **⚠️ 枚举值命名规范**:所有枚举值统一使用 **`lower_snake_case`**(全小写+下划线)。
|
||||||
|
> 历史遗留大写值(`SUCCESS`/`FAILED`、`BOOLEAN`/`SCOPE`/`INTEGER`、`REPLACE`/`RESTRICT`/`GRANT`、`A_urgent`/`A_app`/`B_schema`/`C_feature`)在 v2.2 中已统一迁移为小写。
|
||||||
|
> **DB `CHECK` 约束、`enum_labels` 种子数据、前端代码须同步更新为新值**;迁移脚本须包含历史数据 UPDATE。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
|
||||||
|
|
||||||
|
## 一、枚举分层标准(必须遵守)
|
||||||
|
|
||||||
|
Fonrey 采用两层枚举体系:
|
||||||
|
|
||||||
|
1. **固定枚举(Fixed Enum)**
|
||||||
|
- 值域固定,受 `CHECK` 或强业务约束保护
|
||||||
|
- 作为系统契约,不能随意改值
|
||||||
|
- 可落地到 `public.enum_labels`(用于统一标签)
|
||||||
|
|
||||||
|
2. **可配置枚举(Configurable Enum)**
|
||||||
|
- 值域由租户自行维护
|
||||||
|
- 存储在 Tenant Schema:`lookup_groups` + `lookup_items`
|
||||||
|
- **禁止**对业务表字段加固定 `CHECK IN (...)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、全局固定枚举(Public / 平台级)
|
||||||
|
|
||||||
|
### 2.1 tenant 生命周期
|
||||||
|
|
||||||
|
**domain**: `public.tenant.plan`
|
||||||
|
- `basic`:基础版
|
||||||
|
- `professional`:专业版
|
||||||
|
- `enterprise`:企业版
|
||||||
|
|
||||||
|
**domain**: `public.tenant.status`
|
||||||
|
- `creating`:创建中
|
||||||
|
- `active`:正常
|
||||||
|
- `suspended`:已挂起
|
||||||
|
- `pending_delete`:待删除
|
||||||
|
- `deleted`:已删除
|
||||||
|
- `failed`:创建/初始化失败
|
||||||
|
|
||||||
|
**domain**: `public.tenant.suspended_reason`
|
||||||
|
- `overdue`:欠费
|
||||||
|
- `violation`:违规
|
||||||
|
- `requested`:客户申请
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
### 2.2 平台管理员
|
||||||
|
|
||||||
|
**domain**: `public.platform_admin.role`
|
||||||
|
- `super_admin`:超级管理员
|
||||||
|
- `ops_operator`:运营管理员
|
||||||
|
- `read_only_auditor`:只读审计员
|
||||||
|
|
||||||
|
### 2.3 平台审计与备份导出
|
||||||
|
|
||||||
|
**domain**: `public.platform_audit.result`
|
||||||
|
- `success`:成功
|
||||||
|
- `failed`:失败
|
||||||
|
|
||||||
|
**domain**: `public.backup_schedule.frequency`
|
||||||
|
- `hourly`:每小时
|
||||||
|
- `daily`:每日
|
||||||
|
- `weekly`:每周
|
||||||
|
|
||||||
|
**domain**: `public.backup_schedule.storage_target`
|
||||||
|
- `local`:本地存储
|
||||||
|
- `s3`:Amazon S3
|
||||||
|
- `r2`:Cloudflare R2
|
||||||
|
- `gcs`:Google Cloud Storage
|
||||||
|
|
||||||
|
**domain**: `public.backup_record.trigger_type`
|
||||||
|
- `auto`:自动触发
|
||||||
|
- `manual`:手动触发
|
||||||
|
- `pre_upgrade`:升级前触发
|
||||||
|
- `pre_restore`:恢复前触发
|
||||||
|
|
||||||
|
**domain**: `public.backup_record.status`
|
||||||
|
- `pending`:待执行
|
||||||
|
- `in_progress`:执行中
|
||||||
|
- `success`:成功
|
||||||
|
- `failed`:失败
|
||||||
|
|
||||||
|
**domain**: `public.export_task.format`
|
||||||
|
- `csv`:CSV
|
||||||
|
- `json`:JSON
|
||||||
|
- `sql_dump`:SQL 导出
|
||||||
|
|
||||||
|
**domain**: `public.export_task.status`
|
||||||
|
- `pending`:待执行
|
||||||
|
- `in_progress`:执行中
|
||||||
|
- `success`:成功(语义等价原 `done`,与 `backup_record.status` 对齐)
|
||||||
|
- `failed`:失败
|
||||||
|
|
||||||
|
### 2.4 升级与发布(Public)
|
||||||
|
|
||||||
|
**domain**: `public.upgrade_event.event_type`
|
||||||
|
- `upgrade`:升级
|
||||||
|
- `rollback`:回滚
|
||||||
|
|
||||||
|
**domain**: `public.upgrade_event.upgrade_type`
|
||||||
|
- `app`:A类-应用升级(原 `A_app`)
|
||||||
|
- `schema`:B类-数据库结构升级(原 `B_schema`)
|
||||||
|
- `feature`:C类-功能开关升级(原 `C_feature`)
|
||||||
|
|
||||||
|
**domain**: `public.upgrade_event.strategy`
|
||||||
|
- `full`:全量发布
|
||||||
|
- `canary`:灰度发布
|
||||||
|
|
||||||
|
**domain**: `public.upgrade_event.status`
|
||||||
|
- `draft`:草稿
|
||||||
|
- `pre_check`:预检查
|
||||||
|
- `pre_backup`:预备份
|
||||||
|
- `batch_running`:批次执行中
|
||||||
|
- `batch_done`:批次完成
|
||||||
|
- `halted`:已暂停
|
||||||
|
- `succeeded`:已成功
|
||||||
|
- `failed`:失败
|
||||||
|
- `rollback_running`:回滚中
|
||||||
|
- `rolled_back`:已回滚
|
||||||
|
|
||||||
|
**domain**: `public.upgrade_event.failure_policy`
|
||||||
|
- `halt_batch`:失败即停止批次
|
||||||
|
- `continue`:失败继续
|
||||||
|
|
||||||
|
**domain**: `public.client_release.platform`
|
||||||
|
- `win32`:Windows 客户端
|
||||||
|
|
||||||
|
**domain**: `public.client_release.arch`
|
||||||
|
- `x64`:x64 架构
|
||||||
|
- `arm64`:ARM64 架构
|
||||||
|
|
||||||
|
**domain**: `public.client_release.release_type`
|
||||||
|
- `normal`:普通更新
|
||||||
|
- `force`:强制更新
|
||||||
|
|
||||||
|
**domain**: `public.client_release.status`
|
||||||
|
- `draft`:草稿
|
||||||
|
- `published`:已发布
|
||||||
|
- `archived`:已归档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Tenant 固定枚举(模块级,值域统一)
|
||||||
|
|
||||||
|
> 说明:以下字段在 Tenant Schema 中存储,但值域为系统统一标准,属于“全局实现标准”。
|
||||||
|
|
||||||
|
## 3.1 登录认证(account/login)
|
||||||
|
|
||||||
|
**domain**: `login.user_account.status`
|
||||||
|
- `active`:启用
|
||||||
|
- `disabled`:停用
|
||||||
|
- `locked`:锁定
|
||||||
|
|
||||||
|
**domain**: `login.login_attempt.failure_reason`
|
||||||
|
- `wrong_password`:用户名或密码错误
|
||||||
|
- `wrong_captcha`:验证码错误
|
||||||
|
- `account_locked`:账号锁定
|
||||||
|
- `account_disabled`:账号停用
|
||||||
|
- `tenant_not_found`:租户不存在
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.2 组织人事(org)
|
||||||
|
|
||||||
|
**domain**: `org.org_unit.type`
|
||||||
|
- `company`:公司
|
||||||
|
- `division`:事业部
|
||||||
|
- `region`:大区
|
||||||
|
- `area`:区域
|
||||||
|
- `district`:片区
|
||||||
|
- `store`:门店
|
||||||
|
- `group`:店组
|
||||||
|
- `functional`:职能部门
|
||||||
|
|
||||||
|
**domain**: `org.org_unit.attribute`
|
||||||
|
- `direct`:直营
|
||||||
|
- `franchise`:加盟
|
||||||
|
|
||||||
|
**domain**: `org.staff.role`
|
||||||
|
- `agent`:经纪人
|
||||||
|
- `store_manager`:店长
|
||||||
|
- `area_manager`:区域经理
|
||||||
|
- `admin`:系统管理员
|
||||||
|
- `operator`:运营/行政
|
||||||
|
- `system`:系统账号
|
||||||
|
|
||||||
|
**domain**: `org.staff.status`
|
||||||
|
- `active`:在职
|
||||||
|
- `probation`:试用
|
||||||
|
- `resigned`:离职
|
||||||
|
- `frozen`:冻结
|
||||||
|
|
||||||
|
**domain**: `org.staff_personal_info.gender`
|
||||||
|
- `male`:男
|
||||||
|
- `female`:女
|
||||||
|
- `unknown`:未知
|
||||||
|
|
||||||
|
**domain**: `org.staff_personal_info.id_type`
|
||||||
|
- `id_card`:身份证
|
||||||
|
- `passport`:护照
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `org.staff_transfer.transfer_type`
|
||||||
|
- `onboard`:入职
|
||||||
|
- `transfer`:调动
|
||||||
|
- `resign`:离职
|
||||||
|
- `rejoin`:复职
|
||||||
|
- `supervisor_change`:上级变更
|
||||||
|
- `role_change`:角色变更
|
||||||
|
- `freeze`:冻结账号
|
||||||
|
- `unfreeze`:恢复账号
|
||||||
|
|
||||||
|
**domain**: `org.staff_account.platform`
|
||||||
|
- `fonrey`:房睿主账号
|
||||||
|
- `58anjuke`:58安居客
|
||||||
|
- `cnreic`:中国网络经纪人
|
||||||
|
- `wechat_mp`:微信公众号
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.3 权限系统(permission)
|
||||||
|
|
||||||
|
**domain**: `permission.module`
|
||||||
|
- `home`:首页
|
||||||
|
- `property`:房源
|
||||||
|
- `new_house`:新房
|
||||||
|
- `client`:客源
|
||||||
|
- `transaction`:交易
|
||||||
|
- `data`:数据
|
||||||
|
- `marketing`:营销
|
||||||
|
- `hr`:人事OA
|
||||||
|
- `contract`:合同
|
||||||
|
- `trinet`:三网
|
||||||
|
- `system`:系统
|
||||||
|
- `mobile`:移动端
|
||||||
|
- `smart_store`:智能门店
|
||||||
|
- `recharge`:在线充值
|
||||||
|
|
||||||
|
**domain**: `permission.value_type`
|
||||||
|
- `boolean`:开关型(原 `BOOLEAN`)
|
||||||
|
- `scope`:范围型(原 `SCOPE`)
|
||||||
|
- `integer`:数值型(原 `INTEGER`)
|
||||||
|
|
||||||
|
**domain**: `permission.role_category`
|
||||||
|
- `agent`:置业顾问
|
||||||
|
- `store_manager`:店管
|
||||||
|
- `director`:总经
|
||||||
|
- `operator`:运营/行政
|
||||||
|
- `custom`:自定义
|
||||||
|
|
||||||
|
**domain**: `permission.scope_level`
|
||||||
|
- `none`:无
|
||||||
|
- `self`:本人
|
||||||
|
- `group`:本组
|
||||||
|
- `store`:本门店
|
||||||
|
- `area`:本区域
|
||||||
|
- `region`:本大区
|
||||||
|
- `company`:全公司
|
||||||
|
|
||||||
|
**domain**: `permission.override_mode`
|
||||||
|
- `replace`:覆盖(原 `REPLACE`)
|
||||||
|
- `restrict`:限制(原 `RESTRICT`)
|
||||||
|
- `grant`:授予(原 `GRANT`)
|
||||||
|
|
||||||
|
**domain**: `permission.data_scope_type`
|
||||||
|
- `self`:本人
|
||||||
|
- `group`:本组
|
||||||
|
- `store`:本门店
|
||||||
|
- `area`:本区域
|
||||||
|
- `region`:本大区
|
||||||
|
- `company`:全公司
|
||||||
|
- `custom_unit`:自定义组织单元
|
||||||
|
|
||||||
|
**domain**: `permission.change_log.target_type`
|
||||||
|
- `role`:角色
|
||||||
|
- `role_permission`:角色权限
|
||||||
|
- `staff_role`:员工角色
|
||||||
|
- `staff_override`:员工权限覆盖
|
||||||
|
- `staff_scope`:员工数据范围
|
||||||
|
|
||||||
|
**domain**: `permission.change_log.action`
|
||||||
|
- `create`:创建
|
||||||
|
- `update`:更新
|
||||||
|
- `delete`:删除
|
||||||
|
- `assign`:分配
|
||||||
|
- `revoke`:撤销
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.4 楼盘区域(complex)
|
||||||
|
|
||||||
|
**domain**: `complex.school.type`
|
||||||
|
- `primary`:小学
|
||||||
|
- `middle`:初中
|
||||||
|
- `high`:高中
|
||||||
|
- `k9`:九年一贯制
|
||||||
|
- `k12`:十二年一贯制
|
||||||
|
|
||||||
|
**domain**: `complex.school.nature`
|
||||||
|
- `public`:公立
|
||||||
|
- `private`:私立
|
||||||
|
- `international`:国际
|
||||||
|
|
||||||
|
**domain**: `complex.school.level`
|
||||||
|
- `normal`:普通
|
||||||
|
- `key`:重点
|
||||||
|
- `top`:名校
|
||||||
|
|
||||||
|
**domain**: `complex.building_type`
|
||||||
|
- `slab`:板楼
|
||||||
|
- `tower`:塔楼
|
||||||
|
- `slab_tower`:板塔结合
|
||||||
|
|
||||||
|
**domain**: `complex.water_type`
|
||||||
|
- `civil`:民水
|
||||||
|
- `commercial`:商水
|
||||||
|
|
||||||
|
**domain**: `complex.electricity_type`
|
||||||
|
- `civil`:民电
|
||||||
|
- `commercial`:商电
|
||||||
|
|
||||||
|
**domain**: `complex.school_zone_type`
|
||||||
|
- `guaranteed`:对口
|
||||||
|
- `reference`:参考
|
||||||
|
- `lottery`:摇号
|
||||||
|
|
||||||
|
**domain**: `complex.photo.category`
|
||||||
|
- `complex`:楼盘图
|
||||||
|
- `layout`:户型图
|
||||||
|
- `vr`:VR图
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.5 房源(property)
|
||||||
|
|
||||||
|
**domain**: `property.property_type`
|
||||||
|
- `residential`:住宅
|
||||||
|
- `villa`:别墅
|
||||||
|
- `commercial_residential`:商住
|
||||||
|
- `shop`:商铺
|
||||||
|
- `office`:写字楼
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `property.status`
|
||||||
|
- `for_sale`:出售
|
||||||
|
- `for_rent`:出租
|
||||||
|
- `for_sale_rent`:租售
|
||||||
|
- `suspended`:暂缓
|
||||||
|
- `sold_elsewhere`:他售
|
||||||
|
- `rented_elsewhere`:他租
|
||||||
|
- `sold`:成交
|
||||||
|
- `unlisted`:未挂牌
|
||||||
|
|
||||||
|
**domain**: `property.attribute`
|
||||||
|
- `public`:公盘
|
||||||
|
- `private`:私盘
|
||||||
|
- `special`:特盘
|
||||||
|
- `sealed`:封盘
|
||||||
|
|
||||||
|
**domain**: `property.orientation`
|
||||||
|
- `east`:东
|
||||||
|
- `south`:南
|
||||||
|
- `west`:西
|
||||||
|
- `north`:北
|
||||||
|
- `southeast`:东南
|
||||||
|
- `northeast`:东北
|
||||||
|
- `east_west`:东西
|
||||||
|
- `south_north`:南北
|
||||||
|
- `northwest`:西北
|
||||||
|
- `southwest`:西南
|
||||||
|
|
||||||
|
**domain**: `property.decoration`
|
||||||
|
- `rough`:毛坯
|
||||||
|
- `plain`:清水
|
||||||
|
- `simple`:简装
|
||||||
|
- `medium`:中装
|
||||||
|
- `fine`:精装
|
||||||
|
- `luxury`:豪装
|
||||||
|
|
||||||
|
**domain**: `property.house_status`
|
||||||
|
- `owner_occupied`:业主自住
|
||||||
|
- `vacant`:空置
|
||||||
|
- `tenant_occupied`:租客在住
|
||||||
|
- `unknown`:未知
|
||||||
|
|
||||||
|
**domain**: `property.viewing_time`
|
||||||
|
- `anytime`:随时看房
|
||||||
|
- `by_appointment`:预约看房
|
||||||
|
- `inconvenient`:不便看房
|
||||||
|
|
||||||
|
**domain**: `property.grade`
|
||||||
|
- `a`:A(急迫)
|
||||||
|
- `b`:B(较强)
|
||||||
|
- `c`:C(一般)
|
||||||
|
- `d`:D(较弱)
|
||||||
|
|
||||||
|
**domain**: `property.contact.gender`
|
||||||
|
- `male`:先生
|
||||||
|
- `female`:女士
|
||||||
|
|
||||||
|
**domain**: `property.contact.identity`
|
||||||
|
- `owner`:业主
|
||||||
|
- `contact`:联系人
|
||||||
|
- `subletter`:转租人
|
||||||
|
- `tenant`:租客
|
||||||
|
- `agent`:代理人
|
||||||
|
- `corporate`:企业法人
|
||||||
|
|
||||||
|
**domain**: `property.listing_history.listing_type`
|
||||||
|
- `for_sale`:出售挂牌
|
||||||
|
- `for_rent`:出租挂牌
|
||||||
|
|
||||||
|
**domain**: `property.listing_history.status`
|
||||||
|
- `active`:生效中
|
||||||
|
- `ended`:已结束
|
||||||
|
|
||||||
|
**domain**: `property.follow_log.log_type`
|
||||||
|
- `written`:手写跟进
|
||||||
|
- `modified`:修改跟进
|
||||||
|
- `sensitive_op`:敏感操作
|
||||||
|
- `sensitive_view`:敏感查看
|
||||||
|
- `other`:其他
|
||||||
|
- `system`:系统
|
||||||
|
|
||||||
|
**domain**: `property.follow_log.ai_tag`
|
||||||
|
- `ai_for_sale`:AI判断可售
|
||||||
|
- `ai_not_for_sale`:AI判断不可售
|
||||||
|
|
||||||
|
**domain**: `property.follow_attachment.file_type`
|
||||||
|
- `bmp`:BMP
|
||||||
|
- `jpg`:JPG
|
||||||
|
- `png`:PNG
|
||||||
|
- `svg`:SVG
|
||||||
|
- `gif`:GIF
|
||||||
|
|
||||||
|
**domain**: `property.key.key_type`
|
||||||
|
- `mechanical`:机械钥匙
|
||||||
|
- `password`:密码钥匙
|
||||||
|
|
||||||
|
**domain**: `property.commission.owner_type`
|
||||||
|
- `owner`:产权人本人
|
||||||
|
- `authorized_third`:授权第三方
|
||||||
|
|
||||||
|
**domain**: `property.commission.status`
|
||||||
|
- `active`:有效
|
||||||
|
- `expired`:过期
|
||||||
|
- `cancelled`:取消
|
||||||
|
|
||||||
|
**domain**: `property.commission_attachment.category`
|
||||||
|
- `id_card`:身份证件
|
||||||
|
- `property_cert`:产权证明
|
||||||
|
- `commission_letter`:委托书
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `property.field_survey.status`
|
||||||
|
- `draft`:草稿
|
||||||
|
- `submitted`:已提交
|
||||||
|
|
||||||
|
**domain**: `property.survey_photo.category`
|
||||||
|
- `layout`:户型图
|
||||||
|
- `living_room`:客厅
|
||||||
|
- `dining_room`:餐厅
|
||||||
|
- `bedroom`:卧室
|
||||||
|
- `bathroom`:卫生间
|
||||||
|
- `kitchen`:厨房
|
||||||
|
- `entrance`:入户
|
||||||
|
- `balcony`:阳台
|
||||||
|
- `study`:书房
|
||||||
|
- `indoor_other`:室内其他
|
||||||
|
- `outdoor`:室外
|
||||||
|
|
||||||
|
**domain**: `property.photo.category`
|
||||||
|
- `cover`:封面
|
||||||
|
- `entrance`:入户
|
||||||
|
- `living_room`:客厅
|
||||||
|
- `dining_room`:餐厅
|
||||||
|
- `bedroom`:卧室
|
||||||
|
- `bathroom`:卫生间
|
||||||
|
- `kitchen`:厨房
|
||||||
|
- `balcony`:阳台
|
||||||
|
- `study`:书房
|
||||||
|
- `indoor_other`:室内其他
|
||||||
|
- `outdoor`:室外
|
||||||
|
- `panorama`:全景
|
||||||
|
|
||||||
|
**domain**: `property.attachment.category`
|
||||||
|
- `id_card`:身份证件
|
||||||
|
- `property_cert`:产权证明
|
||||||
|
- `commission_letter`:委托书
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `property.number_holder_approval.status`
|
||||||
|
- `pending`:待审批
|
||||||
|
- `approved`:已通过
|
||||||
|
- `rejected`:已驳回
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.6 客源(client)
|
||||||
|
|
||||||
|
**domain**: `client.client_type`
|
||||||
|
- `private`:私客
|
||||||
|
- `public`:公客
|
||||||
|
- `transacted`:成交客
|
||||||
|
|
||||||
|
**domain**: `client.status`
|
||||||
|
- `buying`:求购
|
||||||
|
- `renting`:求租
|
||||||
|
- `buy_or_rent`:租购
|
||||||
|
- `suspended`:暂缓
|
||||||
|
- `bought`:已购
|
||||||
|
- `rented_done`:已租
|
||||||
|
- `public`:公客
|
||||||
|
- `invalid`:无效
|
||||||
|
|
||||||
|
> **⚠️ 合法组合约束**(`client_type` × `client.status`)
|
||||||
|
> `client_type` 表示客源的"身份类别",`status` 表示客源的"当前业务状态",两者是不同维度。
|
||||||
|
> 合法组合矩阵如下:
|
||||||
|
>
|
||||||
|
> | `client_type` | 允许的 `status` 值 | 禁止的 `status` 值 |
|
||||||
|
> |---|---|---|
|
||||||
|
> | `private`(私客) | `buying` / `renting` / `buy_or_rent` / `suspended` / `invalid` | `public`、`bought`、`rented_done`(未成交不可用终态) |
|
||||||
|
> | `public`(公客) | `public` / `buying` / `renting` / `buy_or_rent` / `suspended` / `invalid` | `bought`、`rented_done` |
|
||||||
|
> | `transacted`(成交客) | `bought` / `rented_done` | 其他所有值(终态不可逆) |
|
||||||
|
>
|
||||||
|
> **实施要求**:
|
||||||
|
> - 服务层(`ClientService`)在状态变更时必须校验组合合法性
|
||||||
|
> - DB 侧可用触发器或 `CHECK` 约束覆盖最关键禁止项(如 `transacted` + 非终态)
|
||||||
|
> - `private → public` 转换须调用专用方法 `transfer_to_public()`,同时修改 `client_type` 和 `status`
|
||||||
|
|
||||||
|
**domain**: `client.grade`
|
||||||
|
- `A`:A(急迫)
|
||||||
|
- `B`:B(较强)
|
||||||
|
- `C`:C(一般)
|
||||||
|
- `D`:D(较弱)
|
||||||
|
- `E`:E(暂不关注)
|
||||||
|
|
||||||
|
**domain**: `client.property_usage`
|
||||||
|
- `residential`:住宅
|
||||||
|
- `villa`:别墅
|
||||||
|
- `commercial_residential`:商住
|
||||||
|
- `shop`:商铺
|
||||||
|
- `office`:写字楼
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `client.buying_purpose`
|
||||||
|
- `rigid`:刚需
|
||||||
|
- `investment`:投资
|
||||||
|
- `school_district`:学区
|
||||||
|
- `upgrade`:改善
|
||||||
|
- `commercial`:商用
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `client.payment_method`
|
||||||
|
- `full`:全额
|
||||||
|
- `mortgage`:商业贷款
|
||||||
|
- `mortgage_fund`:商贷+公积金
|
||||||
|
- `fund`:公积金
|
||||||
|
|
||||||
|
**domain**: `client.properties_owned`
|
||||||
|
- `none`:无
|
||||||
|
- `local_none`:本地无/外地有
|
||||||
|
- `local_has`:本地有
|
||||||
|
|
||||||
|
**domain**: `client.id_type`
|
||||||
|
- `id_card`:身份证
|
||||||
|
- `passport`:护照
|
||||||
|
- `hk_macao`:港澳通行证
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `client.transfer_to_public_type`
|
||||||
|
- `manual`:手动转公
|
||||||
|
- `auto`:自动转公
|
||||||
|
- `marketing_jump`:营销客跳公
|
||||||
|
- `resource_public`:资料客素公
|
||||||
|
|
||||||
|
**domain**: `client.invalid_reason`
|
||||||
|
- `invalid_phone`:号码无效
|
||||||
|
- `peer_agent`:同行
|
||||||
|
- `ad`:广告推销
|
||||||
|
- `no_intent`:无意向
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `client.transacted_type`
|
||||||
|
- `bought`:我购
|
||||||
|
- `rented`:我租
|
||||||
|
|
||||||
|
**domain**: `client.transacted_property_type`
|
||||||
|
- `second_hand`:二手
|
||||||
|
- `new_house`:新房
|
||||||
|
|
||||||
|
**domain**: `client.activity_level`
|
||||||
|
- `new_matched`:新配对
|
||||||
|
- `active_7d`:7日活跃
|
||||||
|
- `active_30d`:30日活跃
|
||||||
|
- `active_90d`:90日活跃
|
||||||
|
- `expiring`:即将过期
|
||||||
|
- `frozen`:暂缓中
|
||||||
|
- `invalid`:无效
|
||||||
|
|
||||||
|
**domain**: `client.contact.gender`
|
||||||
|
- `male`:先生
|
||||||
|
- `female`:女士
|
||||||
|
|
||||||
|
**domain**: `client.requirement_type`
|
||||||
|
- `second_hand`:二手
|
||||||
|
- `new_house`:新房
|
||||||
|
- `rental`:租房
|
||||||
|
|
||||||
|
**domain**: `client.floor_preference`
|
||||||
|
- `no_first`:不要一楼
|
||||||
|
- `low`:低楼层
|
||||||
|
- `mid`:中楼层
|
||||||
|
- `high`:高楼层
|
||||||
|
- `no_top`:不要顶楼
|
||||||
|
|
||||||
|
**domain**: `client.orientation`
|
||||||
|
- `east`:东
|
||||||
|
- `south`:南
|
||||||
|
- `west`:西
|
||||||
|
- `north`:北
|
||||||
|
|
||||||
|
**domain**: `client.decoration`
|
||||||
|
- `rough`:毛坯
|
||||||
|
- `plain`:清水
|
||||||
|
- `simple`:简装
|
||||||
|
- `medium`:中装
|
||||||
|
- `fine`:精装
|
||||||
|
- `luxury`:豪装
|
||||||
|
|
||||||
|
**domain**: `client.building_age_range`
|
||||||
|
- `within_5y`:5年内
|
||||||
|
- `5_10y`:5-10年
|
||||||
|
- `10_15y`:10-15年
|
||||||
|
- `15_20y`:15-20年
|
||||||
|
- `over_20y`:20年以上
|
||||||
|
|
||||||
|
**domain**: `client.follow_log.log_type`
|
||||||
|
- `written`:写入跟进
|
||||||
|
- `modified`:修改跟进
|
||||||
|
- `sensitive_view`:敏感查看
|
||||||
|
- `other`:其他
|
||||||
|
- `system`:系统
|
||||||
|
|
||||||
|
**domain**: `client.viewing.viewing_type`
|
||||||
|
- `appointment`:预约
|
||||||
|
- `viewing`:带看
|
||||||
|
- `revisit`:复看
|
||||||
|
- `empty`:空看
|
||||||
|
|
||||||
|
**domain**: `client.viewing.client_intent`
|
||||||
|
- `interested`:感兴趣
|
||||||
|
- `not_interested`:不感兴趣
|
||||||
|
- `negotiating`:谈判中
|
||||||
|
- `cancelled`:取消
|
||||||
|
|
||||||
|
**domain**: `client.property_match.match_source`
|
||||||
|
- `recorded`:录客配房
|
||||||
|
- `system`:系统配房
|
||||||
|
|
||||||
|
**domain**: `client.property_match.match_group`
|
||||||
|
- `quality_layout`:优质户型
|
||||||
|
- `price_reduced`:降价
|
||||||
|
- `hot`:热门
|
||||||
|
- `newly_listed`:新上
|
||||||
|
|
||||||
|
**domain**: `client.property_match.status`
|
||||||
|
- `suggested`:待推送
|
||||||
|
- `shared`:已分享
|
||||||
|
- `rejected`:已反馈不合适
|
||||||
|
- `viewed`:客户已查看
|
||||||
|
|
||||||
|
**domain**: `client.status_log.change_type`
|
||||||
|
- `status_change`:改状态
|
||||||
|
- `grade_change`:改等级
|
||||||
|
- `to_public`:转公客
|
||||||
|
- `to_transacted`:转成交
|
||||||
|
- `to_invalid`:转无效
|
||||||
|
- `owner_change`:改归属人
|
||||||
|
- `source_change`:改来源
|
||||||
|
- `merge`:合并客源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.7 系统配置(setting)
|
||||||
|
|
||||||
|
**domain**: `setting.tenant_setting.value_type`
|
||||||
|
- `bool`:布尔
|
||||||
|
- `int`:整数
|
||||||
|
- `string`:字符串
|
||||||
|
- `enum`:枚举
|
||||||
|
|
||||||
|
**domain**: `setting.field_rule.module`
|
||||||
|
- `property`:房源
|
||||||
|
- `client`:客源(预留)
|
||||||
|
|
||||||
|
**domain**: `setting.field_rule.entity_type`(与 `property.property_type` 对齐)
|
||||||
|
- `residential`:住宅
|
||||||
|
- `villa`:别墅
|
||||||
|
- `commercial_residential`:商住
|
||||||
|
- `shop`:商铺
|
||||||
|
- `office`:写字楼
|
||||||
|
- `other`:其他
|
||||||
|
|
||||||
|
**domain**: `setting.field_rule.trade_status`
|
||||||
|
- `sale`:出售
|
||||||
|
- `rent`:出租
|
||||||
|
- `sale_rent`:租售
|
||||||
|
- `all`:全部(原设计为 `*`,因 SQL/URL/权限表达式通配误解风险已统一改为 `all`;DB CHECK 约束、前端筛选器须同步)
|
||||||
|
|
||||||
|
**domain**: `setting.field_rule.requirement`
|
||||||
|
- `required`:必填
|
||||||
|
- `optional`:选填
|
||||||
|
- `hidden`:隐藏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Tenant 可配置枚举字段清单(lookup_items 权威)
|
||||||
|
|
||||||
|
> 以下字段值域由 `lookup_items` 维护,属于租户级配置,不在业务表中写死 `CHECK`。
|
||||||
|
|
||||||
|
| domain(统一命名) | 对应字段 | 当前状态 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `client.source` | `clients.source` | ✅ 已落地 | 客源来源 |
|
||||||
|
| `client.follow_purpose` | `client_follow_logs.purpose` | ✅ 已落地 | 客源跟进目的 |
|
||||||
|
| `property.source` | `properties.source` | ✅ 已落地 | 房源来源 |
|
||||||
|
| `property.follow_purpose` | `follow_logs.purpose` | 🔄 建议统一 | 房源跟进目的(建议与 `client.follow_purpose` 共享或独立分组) |
|
||||||
|
| `property.commission_type` | `commissions.commission_type` | 🔄 待入组 | 委托类型(独家/非独家等) |
|
||||||
|
| `client.match_feedback` | `client_property_matches.feedback` | 🔄 待入组 | 配房反馈原因 |
|
||||||
|
| `org.reward_punish_category` | `staff_reward_punish.category` | 🔄 待入组 | 人事奖惩类别 |
|
||||||
|
|
||||||
|
### 4.1 lookup_groups 规范(Tenant Schema)
|
||||||
|
|
||||||
|
- `module`: 业务模块标识(如 `client` / `property` / `org`)
|
||||||
|
- `key`: 领域键(如 `source` / `follow_purpose`)
|
||||||
|
- 同一组内 `value` 不可重复(`UNIQUE(group_id, value)`)
|
||||||
|
- `is_system = TRUE` 的项禁止物理删除(仅可停用)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、统一实现约束
|
||||||
|
|
||||||
|
1. **固定枚举值不可改名**:只能新增或停用,禁止修改既有 value。
|
||||||
|
2. **中文展示从字典取值**:前端/UI 不得硬编码中文。
|
||||||
|
3. **可配置枚举不得加固定 CHECK**:防止租户自定义被数据库约束阻断。
|
||||||
|
4. **跨模块同名枚举必须复用语义**:如 `status` 在不同领域必须使用 domain 区分,不允许混用。
|
||||||
|
5. **缓存规范**:
|
||||||
|
- 固定枚举:`public:enum_labels:{domain}`(建议 TTL 24h)
|
||||||
|
- 可配置枚举:`{schema}:setting:lookup:{module}.{key}`(TTL 300s)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、变更流程(必须同步)
|
||||||
|
|
||||||
|
新增或修改任一枚举时,必须同时更新:
|
||||||
|
|
||||||
|
1. 本文档 `ENUMS.md`
|
||||||
|
2. 对应 `DATA_MODEL_*.md` 字段定义(CHECK / 注释 / 业务规则)
|
||||||
|
3. `enum_labels` 种子数据(若为固定枚举)或 `lookup_groups/items` fixture(若为可配置枚举)
|
||||||
|
4. 服务层缓存失效逻辑(Redis key)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、与 ADR 的一致性说明
|
||||||
|
|
||||||
|
本文件已对齐以下冻结决策:
|
||||||
|
|
||||||
|
- 固定枚举与可配置枚举双轨并存
|
||||||
|
- 状态机和值域以文档为权威来源
|
||||||
|
- Tenant 级可配置枚举统一由 `setting` 模块托管
|
||||||
|
- Agent 编码前先读枚举标准,禁止各模块自行定义“影子枚举”
|
||||||
322
Project/fonrey/DATA_MODEL/STATE_MACHINE.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
> For AI assistants: Read this entire file before writing any code. All decisions here are final.
|
||||||
|
|
||||||
|
# Fonrey — 统一状态机规范 (STATE_MACHINE)
|
||||||
|
|
||||||
|
**定位**:本文档是 Fonrey 项目所有实体生命周期与状态流转的唯一权威定义。
|
||||||
|
**变更历史**:
|
||||||
|
|
||||||
|
| 日期 | 变更人 | 变更内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-06-04 | Sisyphus | 初始版本:整合各模块状态机逻辑,对齐 ENUMS v2.2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 通用约定
|
||||||
|
|
||||||
|
- **状态值命名**:统一采用 `lower_snake_case`,必须与 `DATA_MODEL/ENUMS.md` 完全一致。
|
||||||
|
- **触发动作**:状态迁移必须由显式的动作(Action)或事件(Event)触发。
|
||||||
|
- **操作日志**:所有关键状态迁移(除系统内部中间态外)必须记录对应的 `status_log` 或 `audit_log` 表。
|
||||||
|
- **不可逆规则**:标注为“终态”的状态禁止迁回初始态或中间态。
|
||||||
|
- **权限要求**:状态迁移接口必须校验操作者的 DataScope 与 Function Permission。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 租户生命周期 (Public.Tenant)
|
||||||
|
|
||||||
|
**实体**: `public.tenants` | **字段**: `status`
|
||||||
|
**参考**: `DATA_MODEL_PUBLIC.md` §2.1, §4.1
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> creating
|
||||||
|
creating --> active: 初始化完成
|
||||||
|
creating --> failed: 初始化失败
|
||||||
|
active --> suspended: 逾期/违规/申请
|
||||||
|
suspended --> active: 恢复条件满足
|
||||||
|
active --> pending_delete: 申请注销
|
||||||
|
suspended --> pending_delete: 申请注销
|
||||||
|
pending_delete --> deleted: 30天后/人工确认
|
||||||
|
deleted --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态枚举值
|
||||||
|
(domain: `public.tenant.status`)
|
||||||
|
- `creating`: 创建中
|
||||||
|
- `active`: 正常
|
||||||
|
- `suspended`: 已挂起
|
||||||
|
- `pending_delete`: 待删除
|
||||||
|
- `deleted`: 已删除
|
||||||
|
- `failed`: 创建/初始化失败
|
||||||
|
|
||||||
|
### 状态迁移表
|
||||||
|
|
||||||
|
| 当前状态 | 下一状态 | 触发动作/事件 | 触发角色 | 副作用 | 是否可逆 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `creating` | `active` | 租户 Schema 初始化成功 | System | 写 `tenant_status_logs` | 否 |
|
||||||
|
| `creating` | `failed` | 初始化脚本执行失败 | System | 记录错误日志 | 否 |
|
||||||
|
| `active` | `suspended` | 欠费/违规/License过期 | System/SuperAdmin | 设置 `suspended_until`, 写日志 | 是 |
|
||||||
|
| `suspended` | `active` | 缴费成功/解封/到期恢复 | System/SuperAdmin | 清除挂起原因 | 是 |
|
||||||
|
| `active`/`suspended` | `pending_delete` | 提交注销申请 | TenantAdmin | 设置 `deleted_at` | 是 |
|
||||||
|
| `pending_delete` | `deleted` | 确认注销/静默期结束 | SuperAdmin/System | 物理删除或深度标记 | 否 |
|
||||||
|
|
||||||
|
**来源章节**: `DATA_MODEL_PUBLIC.md` §4.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 客源状态机 (Client)
|
||||||
|
|
||||||
|
**实体**: `client.clients` | **字段**: `status`
|
||||||
|
**参考**: `DATA_MODEL_CLIENT.md` §3.1, §4 | `ENUMS.md` §3.6
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> buying
|
||||||
|
[*] --> renting
|
||||||
|
[*] --> buy_or_rent
|
||||||
|
|
||||||
|
state "活跃态" as Active {
|
||||||
|
buying
|
||||||
|
renting
|
||||||
|
buy_or_rent
|
||||||
|
}
|
||||||
|
|
||||||
|
Active --> suspended: 暂时无需求
|
||||||
|
suspended --> Active: 重新激活
|
||||||
|
|
||||||
|
Active --> public: 手动转公/超时自动
|
||||||
|
suspended --> public: 手动转公
|
||||||
|
|
||||||
|
Active --> bought: 转成交(我购)
|
||||||
|
Active --> rented_done: 转成交(我租)
|
||||||
|
|
||||||
|
Active --> invalid: 转无效
|
||||||
|
suspended --> invalid: 转无效
|
||||||
|
|
||||||
|
invalid --> Active: 经理审批恢复
|
||||||
|
|
||||||
|
bought --> [*]
|
||||||
|
rented_done --> [*]
|
||||||
|
public --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态枚举值
|
||||||
|
(domain: `client.status`)
|
||||||
|
- `buying`: 求购
|
||||||
|
- `renting`: 求租
|
||||||
|
- `buy_or_rent`: 租购
|
||||||
|
- `suspended`: 暂缓
|
||||||
|
- `bought`: 已购 (我购)
|
||||||
|
- `rented_done`: 已租 (我租)
|
||||||
|
- `public`: 公客
|
||||||
|
- `invalid`: 无效
|
||||||
|
|
||||||
|
### 状态迁移表
|
||||||
|
|
||||||
|
| 当前状态 | 下一状态 | 触发动作/事件 | 触发角色 | 副作用 | 是否可逆 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `buying`/`renting`/`buy_or_rent` | `suspended` | 修改状态为暂缓 | 经纪人 | 写 `client_status_logs` | 是 |
|
||||||
|
| `suspended` | `buying`/`renting`/`buy_or_rent` | 重新激活需求 | 经纪人 | 更新 `last_active_at` | 是 |
|
||||||
|
| `Active`/`suspended` | `public` | 手动转公/超时自动转公 | 经纪人/System | 修改 `client_type='public'` | 否 |
|
||||||
|
| `Active` | `bought`/`rented_done` | 录入成交合同 | 经纪人 | 修改 `client_type='transacted'` | 否 |
|
||||||
|
| `Active`/`suspended` | `invalid` | 标记无效 | 经纪人 | 记录 `invalid_reason` | 需审批 |
|
||||||
|
|
||||||
|
### 禁迁规则
|
||||||
|
- `transacted` (bought/rented_done) 状态为绝对终态,禁止迁往任何其他状态。
|
||||||
|
- `public` 状态客源禁止直接修改为 `private` 状态下的需求状态,必须走领用流程。
|
||||||
|
|
||||||
|
**来源章节**: `DATA_MODEL_CLIENT.md` §4, `ENUMS.md` §3.6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 房源状态机 (Property)
|
||||||
|
|
||||||
|
**实体**: `property.properties` | **字段**: `status`
|
||||||
|
**参考**: `DATA_MODEL_PROPERTY.md` §3, §4.1
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> unlisted
|
||||||
|
unlisted --> for_sale: 挂牌出售
|
||||||
|
unlisted --> for_rent: 挂牌出租
|
||||||
|
|
||||||
|
state "在盘态" as Listed {
|
||||||
|
for_sale
|
||||||
|
for_rent
|
||||||
|
for_sale_rent
|
||||||
|
}
|
||||||
|
|
||||||
|
Listed --> suspended: 业主暂缓
|
||||||
|
suspended --> Listed: 重新上架
|
||||||
|
|
||||||
|
Listed --> sold: 本司成交
|
||||||
|
Listed --> sold_elsewhere: 他售
|
||||||
|
Listed --> rented_elsewhere: 他租
|
||||||
|
|
||||||
|
Listed --> unlisted: 下架
|
||||||
|
|
||||||
|
sold --> [*]
|
||||||
|
sold_elsewhere --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态枚举值
|
||||||
|
(domain: `property.status`)
|
||||||
|
- `for_sale`: 出售
|
||||||
|
- `for_rent`: 出租
|
||||||
|
- `for_sale_rent`: 租售
|
||||||
|
- `suspended`: 暂缓
|
||||||
|
- `sold_elsewhere`: 他售
|
||||||
|
- `rented_elsewhere`: 他租
|
||||||
|
- `sold`: 成交
|
||||||
|
- `unlisted`: 未挂牌
|
||||||
|
|
||||||
|
### 状态迁移表
|
||||||
|
|
||||||
|
| 当前状态 | 下一状态 | 触发动作/事件 | 触发角色 | 副作用 | 是否可逆 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `unlisted` | `for_sale`/`for_rent` | 房源挂牌 | 经纪人 | 新增 `listing_histories` | 是 |
|
||||||
|
| `Listed` | `suspended` | 业主通知暂缓 | 经纪人 | 写跟进日志 | 是 |
|
||||||
|
| `Listed` | `sold` | 本司录入成交 | 经纪人 | 自动结束挂牌历史 | 否 |
|
||||||
|
| `Listed` | `sold_elsewhere`/`rented_elsewhere` | 核实他司已成交 | 经纪人 | | 否 |
|
||||||
|
| `Listed` | `unlisted` | 强制下架 | 管理员 | | 是 |
|
||||||
|
|
||||||
|
**来源章节**: `DATA_MODEL_PROPERTY.md` §3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 客户/房源等级 (Grade)
|
||||||
|
|
||||||
|
**实体**: `client.clients` / `property.properties` | **字段**: `grade`
|
||||||
|
**参考**: `DATA_MODEL_CLIENT.md` §3.1 | `DATA_MODEL_PROPERTY.md` §4.1
|
||||||
|
|
||||||
|
### 状态枚举值
|
||||||
|
(domain: `client.grade` / `property.grade`)
|
||||||
|
- `A`: A (急迫)
|
||||||
|
- `B`: B (较强)
|
||||||
|
- `C`: C (一般)
|
||||||
|
- `D`: D (较弱)
|
||||||
|
- `E`: E (暂不关注) - 仅客源
|
||||||
|
|
||||||
|
### 状态迁移表
|
||||||
|
|
||||||
|
| 当前状态 | 下一状态 | 触发动作/事件 | 触发角色 | 副作用 | 是否可逆 |
|
||||||
|
| ---- | ------------------- | ------- | ---- | ----------------------------- | ---- |
|
||||||
|
| 任意状态 | `A`/`B`/`C`/`D`/`E` | 手动调整等级 | 经纪人 | 写 `status_log` / `follow_log` | 是 |
|
||||||
|
|
||||||
|
**来源章节**: `DATA_MODEL_CLIENT.md` §4, `DATA_MODEL_PROPERTY.md` §3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 账号状态机 (Account)
|
||||||
|
|
||||||
|
**实体**: `login.user_accounts` | **字段**: `status`
|
||||||
|
**参考**: `DATA_MODEL_LOGIN.md` §5.1
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> active: 创建(初始密码)
|
||||||
|
active --> locked: 密码错误≥5次
|
||||||
|
locked --> active: 30min到期/手动解锁
|
||||||
|
active --> disabled: 离职/手动停用
|
||||||
|
disabled --> active: 复职/手动恢复
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态枚举值
|
||||||
|
(domain: `login.user_account.status`)
|
||||||
|
- `active`: 启用
|
||||||
|
- `disabled`: 停用
|
||||||
|
- `locked`: 锁定
|
||||||
|
|
||||||
|
### 状态迁移表
|
||||||
|
|
||||||
|
| 当前状态 | 下一状态 | 触发动作/事件 | 触发角色 | 副作用 | 是否可逆 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `active` | `locked` | 密码连续错误5次 | System | 设置 `locked_until` | 是 |
|
||||||
|
| `locked` | `active` | 时间到期/管理员解锁 | System/Admin | | 是 |
|
||||||
|
| `active` | `disabled` | 员工离职/手动停用 | Admin/System | 踢出所有 Session | 是 |
|
||||||
|
|
||||||
|
**来源章节**: `DATA_MODEL_LOGIN.md` §5.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 升级事件状态机 (Upgrade Event)
|
||||||
|
|
||||||
|
**实体**: `public.upgrade_events` | **字段**: `status`
|
||||||
|
**参考**: `DATA_MODEL_PUBLIC.md` §4.2
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> draft
|
||||||
|
draft --> pre_check: 提交
|
||||||
|
pre_check --> pre_backup: 健康检查通过
|
||||||
|
pre_backup --> batch_running: 备份完成
|
||||||
|
batch_running --> batch_done: 批次租户成功
|
||||||
|
batch_done --> batch_running: 下一批次
|
||||||
|
batch_done --> succeeded: 全部完成
|
||||||
|
|
||||||
|
batch_running --> halted: 失败/门控拦截
|
||||||
|
halted --> batch_running: 人工继续
|
||||||
|
halted --> rollback_running: 人工回滚
|
||||||
|
rollback_running --> rolled_back: 回滚完成
|
||||||
|
|
||||||
|
batch_running --> failed: 严重故障
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态枚举值
|
||||||
|
(domain: `public.upgrade_event.status`)
|
||||||
|
- `draft`: 草稿
|
||||||
|
- `pre_check`: 预检查
|
||||||
|
- `pre_backup`: 预备份
|
||||||
|
- `batch_running`: 批次执行中
|
||||||
|
- `batch_done`: 批次完成
|
||||||
|
- `halted`: 已暂停
|
||||||
|
- `succeeded`: 已成功
|
||||||
|
- `failed`: 失败
|
||||||
|
- `rollback_running`: 回滚中
|
||||||
|
- `rolled_back`: 已回滚
|
||||||
|
|
||||||
|
**来源章节**: `DATA_MODEL_PUBLIC.md` §4.2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 其他辅助状态
|
||||||
|
|
||||||
|
### 8.1 备份/导出任务 (Backup/Export)
|
||||||
|
**字段**: `status` | **参考**: `ENUMS.md` §2.3
|
||||||
|
- `pending` -> `in_progress` -> `success` / `failed`
|
||||||
|
|
||||||
|
### 8.2 客户端发布 (Client Release)
|
||||||
|
**字段**: `status` | **参考**: `ENUMS.md` §2.4
|
||||||
|
- `draft` -> `published` -> `archived`
|
||||||
|
- **约束**: 同平台架构下仅允许一个 `published`。
|
||||||
|
|
||||||
|
### 8.3 员工状态 (Staff)
|
||||||
|
**字段**: `status` | **参考**: `DATA_MODEL_ORG.md` §4
|
||||||
|
- `probation` -> `active` -> `resigned`
|
||||||
|
- `active` <-> `frozen`
|
||||||
|
|
||||||
|
### 8.4 审批流 (Approval)
|
||||||
|
**实体**: `number_holder_approvals` | **字段**: `status`
|
||||||
|
- `pending` -> `approved` / `rejected`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## X. 状态机一致性校验清单
|
||||||
|
|
||||||
|
1. **枚举同步**: 状态字段值是否在 `ENUMS.md` 中定义且拼写一致?
|
||||||
|
2. **日志完备**: 每次 `status` 变更是否伴随对应的 `status_log` 插入动作?
|
||||||
|
3. **并发控制**: 状态变更是否包含 `version` 乐观锁检查?
|
||||||
|
4. **终态拦截**: 业务逻辑是否显式禁止从终态(如 `sold`, `deleted`, `rolled_back`)向外迁移?
|
||||||
|
5. **副作用触发**: 状态变更相关的异步任务(如转公客通知、缓存清理)是否已在 Service 层注册?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待澄清问题
|
||||||
|
|
||||||
|
1. **Client Invalid 恢复路径**: `DATA_MODEL_CLIENT.md` §4 提到“需经理审批后可恢复”,但未定义恢复后的目标状态(是恢复到 `suspended` 还是之前的活跃态?)。
|
||||||
|
- *涉及文件*: `DATA_MODEL_CLIENT.md`
|
||||||
|
2. **Property Unlisted 属性**: 房源在 `unlisted` 状态下,其 `attribute` (公/私盘) 是否有意义?若从 `private` 挂牌变为 `unlisted`,再次挂牌时是否应默认保留 `private` 属性?
|
||||||
|
- *涉及文件*: `DATA_MODEL_PROPERTY.md`
|
||||||
|
3. **Tenant Deleted 物理性**: `DATA_MODEL_PUBLIC.md` §4.1 提到“硬删除直接到 deleted”,但在 §2.1 又说“硬删除直接物理删除行”。需要明确 `deleted` 状态是代表软删除的终点还是物理删除前的标记。
|
||||||
|
- *涉及文件*: `DATA_MODEL_PUBLIC.md`
|
||||||
|
4. **Staff Resigned 恢复**: 离职员工复职(`rejoin`)后,其 `status` 是回到 `probation` 还是 `active`?
|
||||||
|
- *涉及文件*: `DATA_MODEL_ORG.md`
|
||||||
BIN
Project/fonrey/DATA_MODEL/diagram/fonrey-data-model.drawio.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
645
Project/fonrey/DATA_MODEL/diagram/fonrey-data-model.xml
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
<mxfile host="drawio.ishenwei.online" agent="OpenCode">
|
||||||
|
<diagram name="Fonrey ER Diagram" id="fonrey-er-v1">
|
||||||
|
<mxGraphModel dx="9516" dy="5600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="3300" pageHeight="2340" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="region-org" parent="1" style="swimlane;startSize=30;fillColor=#0d3349;strokeColor=#22d3ee;fontColor=#22d3ee;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" value="ORG / HR" vertex="1">
|
||||||
|
<mxGeometry height="760" width="380" x="-860" y="60" as="geometry">
|
||||||
|
<mxRectangle height="30" width="100" x="40" y="60" as="alternateBounds" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="org-units" parent="region-org" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>org_units</b>
<hr/>
🔑 PK id: uuid
parent_id: uuid (FK → self)
type: varchar(20)
name: varchar(100)
path: varchar(500) [物化路径]
depth: smallint
sort_order: int
is_active: bool
created_at: timestamptz
deleted_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="185" width="280" x="30" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="staff" parent="region-org" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>staff</b>
<hr/>
🔑 PK id: uuid
FK org_unit_id → org_units
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
id_no_enc: text [AES]
user_id: uuid [FK → auth_user]
entry_date: date
status: active/resigned/...
is_active: bool
created_at: timestamptz
deleted_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="215" width="280" x="30" y="280" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-self" edge="1" parent="region-org" source="org-units" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="org-units">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="340" y="153" />
|
||||||
|
<mxPoint x="340" y="108" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-self-lbl" connectable="0" parent="e-org-self" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" value="自引用 parent_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" x="0.1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-staff" edge="1" parent="region-org" source="org-units" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="staff">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-staff-lbl" connectable="0" parent="e-org-staff" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="region-complex" parent="1" style="swimlane;startSize=30;fillColor=#063b2f;strokeColor=#34d399;fontColor=#34d399;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" value="REGION & COMPLEX" vertex="1">
|
||||||
|
<mxGeometry height="1830" width="1200" x="-440" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="districts" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>districts</b>
<hr/>
🔑 PK id: uuid
city: varchar(50)
name: varchar(50)
short_name: varchar(20)
sort_order: int
is_active: bool
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="150" width="280" x="50" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="business-areas" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>business_areas</b>
<hr/>
🔑 PK id: uuid
🔗 FK district_id → districts
name: varchar(100)
latitude: numeric(10,7)
longitude: numeric(10,7)
sort_order: int
is_active: bool" vertex="1">
|
||||||
|
<mxGeometry height="155" width="280" x="50" y="265" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="schools" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>schools</b>
<hr/>
🔑 PK id: uuid
🔗 FK district_id → districts
name: varchar(100)
type: primary/middle/high/k9/k12
nature: public/private/international
level: normal/key/top
is_active: bool" vertex="1">
|
||||||
|
<mxGeometry height="155" width="290" x="490" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complexes" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>complexes</b>
<hr/>
🔑 PK id: uuid
🔗 FK district_id → districts
🔗 FK created_by → staff
name: varchar(200) [⚠ 不可直接修改]
address: varchar(500) [只读]
address_summary: varchar(100)
latitude: numeric(10,7)
longitude: numeric(10,7)
property_usage_types: varchar[]
building_structure: varchar(30)
building_type: slab/tower/slab_tower
land_use_years: varchar(30)
built_years: smallint[]
total_units: int
total_households: int
total_floor_area: numeric(12,2)
plot_area: numeric(12,2)
plot_ratio: numeric(5,2)
green_rate: numeric(5,2)
developer: varchar(200)
property_company: varchar(200)
property_fee: numeric(8,2)
property_phone: varchar(30)
parking_total: int
parking_underground: int
parking_ratio: varchar(20)
water_type: civil/commercial
electricity_type: civil/commercial
has_central_heating: bool
has_gas: bool
lock_building: bool
lock_room: bool
lock_info: bool
lock_standard_room: bool
search_vector: tsvector
remarks: text
is_active: bool
created_at: timestamptz
updated_at: timestamptz
deleted_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="570" width="340" x="30" y="660" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complex-aliases" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>complex_aliases</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
alias: varchar(200)
is_system: bool [系统别名只读]
created_at: timestamptz
🔗 FK created_by → staff" vertex="1">
|
||||||
|
<mxGeometry height="130" width="290" x="475" y="450" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complex-biz-areas" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" value="<b>complex_business_areas</b> [N:M join]
<hr/>
🔗 FK complex_id → complexes
🔗 FK business_area_id → business_areas
is_primary: bool [UNIQUE where TRUE]" vertex="1">
|
||||||
|
<mxGeometry height="110" width="380" x="30" y="480" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complex-schools" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" value="<b>complex_schools</b> [N:M join]
<hr/>
🔗 FK complex_id → complexes
🔗 FK school_id → schools
zone_type: guaranteed/reference/lottery" vertex="1">
|
||||||
|
<mxGeometry height="110" width="310" x="480" y="270" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complex-price-trends" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>complex_price_trends</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
record_month: date [存月份1日]
avg_sale_price: numeric(12,2)
avg_unit_price: numeric(10,2)
transaction_count: int
listing_count: int
created_at: timestamptz
UNIQUE(complex_id, record_month)" vertex="1">
|
||||||
|
<mxGeometry height="185" width="380" x="500" y="980" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complex-photos" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>complex_photos</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
category: complex/layout/vr/other
file_key: text [R2/S3]
thumbnail_key: text
file_name: varchar(255)
file_size: int
width, height: int
is_cover: bool [UNIQUE where TRUE]
sort_order: smallint
created_at: timestamptz
🔗 FK created_by → staff" vertex="1">
|
||||||
|
<mxGeometry height="205" width="300" x="470" y="650" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-biz" edge="1" parent="region-complex" source="districts" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="business-areas">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-biz-lbl" connectable="0" parent="e-dist-biz" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-school" edge="1" parent="region-complex" source="districts" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="schools">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-school-lbl" connectable="0" parent="e-dist-school" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-complex" edge="1" parent="region-complex" source="districts" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="complexes">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-complex-lbl" connectable="0" parent="e-dist-complex" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-biz-join" edge="1" parent="region-complex" source="business-areas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" target="complex-biz-areas">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-join-complex" edge="1" parent="region-complex" source="complex-biz-areas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" target="complexes">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-school-join" edge="1" parent="region-complex" source="schools" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" target="complex-schools">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-school-join2" edge="1" parent="region-complex" source="complex-schools" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" target="complexes">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="420" y="325" />
|
||||||
|
<mxPoint x="420" y="690" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-alias" edge="1" parent="region-complex" source="complexes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="complex-aliases">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="820" y="945" />
|
||||||
|
<mxPoint x="820" y="515" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-alias-lbl" connectable="0" parent="e-complex-alias" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-photos" edge="1" parent="region-complex" source="complexes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="complex-photos">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-photos-lbl" connectable="0" parent="e-complex-photos" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-trend" edge="1" parent="region-complex" source="complexes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="complex-price-trends">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="400" y="1072" />
|
||||||
|
<mxPoint x="400" y="1072" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-trend-lbl" connectable="0" parent="e-complex-trend" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="room-units" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>room_units</b>
<hr/>
🔑 PK id: uuid
🔗 FK building_id → buildings
floor: smallint
floor_name: varchar(20)
room_no: varchar(30)
display_no: varchar(50)
is_standard: bool
is_active: bool
created_at: timestamptz
updated_at: timestamptz
UNIQUE(building_id, floor, room_no)" vertex="1">
|
||||||
|
<mxGeometry height="200" width="310" x="860" y="1402.5" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="metro-lines" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>metro_lines</b>
<hr/>
🔑 PK id: uuid
city: varchar(50)
name: varchar(50)
color: varchar(7) [HEX]
sort_order: int
is_active: bool" vertex="1">
|
||||||
|
<mxGeometry height="130" width="260" x="10" y="1680" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="metro-stations" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>metro_stations</b>
<hr/>
🔑 PK id: uuid
🔗 FK metro_line_id → metro_lines
name: varchar(50)
latitude: numeric(10,7)
longitude: numeric(10,7)
sort_order: int
is_active: bool" vertex="1">
|
||||||
|
<mxGeometry height="150" width="280" x="300" y="1670" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-line-station" edge="1" parent="region-complex" source="metro-lines" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="metro-stations">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-lbl" connectable="0" parent="e-metro-line-station" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="complex-metro-stations" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" value="<b>complex_metro_stations</b> [N:M join]
<hr/>
🔗 FK complex_id → complexes
🔗 FK station_id → metro_stations
distance_meters: int [步行距离]" vertex="1">
|
||||||
|
<mxGeometry height="130" width="330" x="10" y="1520" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-join1" edge="1" parent="region-complex" source="metro-stations" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" target="complex-metro-stations">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-join2" edge="1" parent="region-complex" source="complex-metro-stations" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" target="complexes">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="buildings" parent="region-complex" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>buildings</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
🔗 FK school_id → schools [楼栋级学区]
name: varchar(50)
is_standard: bool
property_usage_type: varchar(20)
built_year: smallint
total_floors: smallint
land_use_years: varchar(30)
has_elevator: bool
is_active: bool
created_at: timestamptz
🔗 FK created_by → staff" vertex="1">
|
||||||
|
<mxGeometry height="225" width="310" x="500" y="1390" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-room" edge="1" parent="region-complex" source="buildings" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="room-units">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-room-lbl" connectable="0" parent="e-bldg-room" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-bldg" edge="1" parent="region-complex" source="complexes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="buildings">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="270" y="1502" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-bldg-lbl" connectable="0" parent="e-complex-bldg" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="region-property" parent="1" style="swimlane;startSize=30;fillColor=#2d1a5e;strokeColor=#a78bfa;fontColor=#a78bfa;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" value="PROPERTY" vertex="1">
|
||||||
|
<mxGeometry height="1700" width="1380" x="800" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="properties" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>properties</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
🔗 FK building_id → buildings
🔗 FK room_unit_id → room_units
🔗 FK agent_id → staff
listing_type: sale/rent/both
status: varchar(20)
sale_price: numeric(12,2) [万元]
rent_price: numeric(10,2) [元/月]
area: numeric(8,2) [m²]
floor: smallint
total_floors: smallint
bedroom: smallint
living_room: smallint
bathroom: smallint
orientation: varchar(30)
decoration: varchar(20)
has_elevator: bool
built_year: smallint
ownership_years: varchar(20)
is_exclusive: bool [独家委托]
completeness_score: int
search_vector: tsvector
source: varchar(30)
remarks: text
created_at: timestamptz
updated_at: timestamptz
deleted_at: timestamptz
🔗 FK created_by → staff
🔗 FK updated_by → staff
<i>[89,000+ rows · 复合索引 · 分区预留]</i>" vertex="1">
|
||||||
|
<mxGeometry height="560" width="380" x="30" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-contacts" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_contacts</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
role: owner/agent/tenant
is_primary: bool
created_at: timestamptz
deleted_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="170" width="310" x="460" y="350" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-follow-logs" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_follow_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK staff_id → staff
log_type: call/visit/price_change/note/...
content: text
phone_no_viewed: bool [敏感操作]
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" vertex="1">
|
||||||
|
<mxGeometry height="185" width="380" x="980" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="listing-histories" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>listing_histories</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
listed_at: timestamptz
delisted_at: timestamptz
list_price: numeric(12,2)
reason: varchar(50)
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="155" width="310" x="980" y="250" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-photos" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_photos</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
category: listing/vr/layout/other
file_key: text [R2/S3]
thumbnail_key: text
is_cover: bool
sort_order: smallint
width, height: int
file_size: int
created_at: timestamptz
🔗 FK created_by → staff" vertex="1">
|
||||||
|
<mxGeometry height="205" width="310" x="30" y="740" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-keys" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_keys</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK holder_id → staff
key_no: varchar(50)
status: held/returned
taken_at: timestamptz
returned_at: timestamptz
notes: text" vertex="1">
|
||||||
|
<mxGeometry height="165" width="310" x="980" y="430" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-commissions" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_commissions</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
commission_type: exclusive/open
rate: numeric(5,4)
amount: numeric(12,2)
start_date: date
end_date: date
signed_at: timestamptz
document_key: text
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="185" width="330" x="980" y="620" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-inspections" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_inspections</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK staff_id → staff
inspected_at: timestamptz
status: pending/done/cancelled
notes: text
attachments: jsonb
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="165" width="320" x="450" y="575" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-marketing" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_marketing</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
title: varchar(200)
highlights: text[]
description: text
tags: varchar[]
platforms: jsonb
published_at: timestamptz
updated_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="175" width="340" x="980" y="870" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="property-certificates" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>property_certificates</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
cert_no: varchar(50)
owner_name: varchar(100)
ownership_type: varchar(30)
area_registered: numeric(8,2)
issue_date: date
document_key: text" vertex="1">
|
||||||
|
<mxGeometry height="165" width="330" x="450" y="790" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="completeness-scores" parent="region-property" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>completeness_scores</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
score: int [0-100]
missing_fields: text[]
calculated_at: timestamptz
version: int" vertex="1">
|
||||||
|
<mxGeometry height="135" width="310" x="980" y="1090" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-contact" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="property-contacts">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="435" y="280" />
|
||||||
|
<mxPoint x="435" y="435" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-contact-lbl" connectable="0" parent="e-prop-contact" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-follow" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="property-follow-logs">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="840" y="80" />
|
||||||
|
<mxPoint x="840" y="80" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-follow-lbl" connectable="0" parent="e-prop-follow" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-listing" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="listing-histories">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="960" y="110" />
|
||||||
|
<mxPoint x="960" y="328" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-listing-lbl" connectable="0" parent="e-prop-listing" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-photos" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="property-photos">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="50" y="672" />
|
||||||
|
<mxPoint x="50" y="672" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-photos-lbl" connectable="0" parent="e-prop-photos" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-keys" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="property-keys">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="920" y="140" />
|
||||||
|
<mxPoint x="920" y="512" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-keys-lbl" connectable="0" parent="e-prop-keys" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-comm" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="property-commissions">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="880" y="180" />
|
||||||
|
<mxPoint x="880" y="712" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-comm-lbl" connectable="0" parent="e-prop-comm" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-insp" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="property-inspections">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="440" y="500" />
|
||||||
|
<mxPoint x="440" y="680" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-insp-lbl" connectable="0" parent="e-prop-insp" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-marketing" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" target="property-marketing">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="830" y="220" />
|
||||||
|
<mxPoint x="830" y="958" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-marketing-lbl" connectable="0" parent="e-prop-marketing" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:1" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-cert" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" target="property-certificates">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="290" y="670" />
|
||||||
|
<mxPoint x="410" y="670" />
|
||||||
|
<mxPoint x="410" y="872" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-cert-lbl" connectable="0" parent="e-prop-cert" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:1" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-score" edge="1" parent="region-property" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" target="completeness-scores">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="790" y="260" />
|
||||||
|
<mxPoint x="790" y="1158" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-score-lbl" connectable="0" parent="e-prop-score" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:1" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="region-client" parent="1" style="swimlane;startSize=30;fillColor=#3d1f06;strokeColor=#fbbf24;fontColor=#fbbf24;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" value="CLIENT" vertex="1">
|
||||||
|
<mxGeometry height="1380" width="1060" x="2280" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="clients" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>clients</b>
<hr/>
🔑 PK id: uuid
🔗 FK agent_id → staff
client_type: private/public/closed
status: active/inactive/converted
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
budget_min/max: numeric
activity_level: 1-5 [Celery每日计算]
is_protected: bool [防自动转公客]
transfer_to_public_type: auto/manual
last_follow_at: timestamptz
source: varchar(30)
remarks: text
created_at: timestamptz
deleted_at: timestamptz
🔗 FK created_by → staff
<i>[私客/公客/成交客 三态状态机]</i>" vertex="1">
|
||||||
|
<mxGeometry height="360" width="370" x="30" y="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-requirements" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_requirements</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
req_type: second_hand/new/rent
district_ids: uuid[]
business_area_ids: uuid[]
price_min: numeric
price_max: numeric
area_min: numeric
area_max: numeric
bedrooms: int[]
school_ids: uuid[]
has_elevator: bool
is_active: bool
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="260" width="350" x="40" y="507.5" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-follow-logs" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_follow_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK staff_id → staff
log_type: call/visit/match/note/status_change
content: text
next_follow_date: date
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" vertex="1">
|
||||||
|
<mxGeometry height="200" width="380" x="660" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-viewings" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_viewings</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK property_id → properties
🔗 FK agent_id → staff
viewed_at: timestamptz
feedback: text
rating: smallint [1-5]
status: planned/done/cancelled
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="195" width="360" x="660" y="310" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-matches" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_property_matches</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK property_id → properties
🔗 FK agent_id → staff
match_type: system/manual
score: numeric(5,2)
status: pending/sent/viewed/dismissed
sent_at: timestamptz
viewed_at: timestamptz
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="205" width="380" x="30" y="800" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-status-logs" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_status_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
from_status: varchar(20)
to_status: varchar(20)
transfer_type: auto/manual
reason: text
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" vertex="1">
|
||||||
|
<mxGeometry height="195" width="370" x="655" y="540" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-fav-folders" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_favorite_folders</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
name: varchar(100)
sort_order: int
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="130" width="300" x="655" y="980" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-folder-items" parent="region-client" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>client_folder_items</b>
<hr/>
🔑 PK id: uuid
🔗 FK folder_id → client_favorite_folders
🔗 FK property_id → properties
sort_order: int
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="130" width="320" x="655" y="780" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-req" edge="1" parent="region-client" source="clients" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-requirements">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="290" y="460" />
|
||||||
|
<mxPoint x="290" y="460" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-req-lbl" connectable="0" parent="e-client-req" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-follow" edge="1" parent="region-client" source="clients" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-follow-logs">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="600" y="80" />
|
||||||
|
<mxPoint x="600" y="80" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-follow-lbl" connectable="0" parent="e-client-follow" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-viewing" edge="1" parent="region-client" source="clients" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-viewings">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="640" y="110" />
|
||||||
|
<mxPoint x="640" y="408" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-viewing-lbl" connectable="0" parent="e-client-viewing" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-match" edge="1" parent="region-client" source="clients" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-matches">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="440" y="300" />
|
||||||
|
<mxPoint x="440" y="902" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-match-lbl" connectable="0" parent="e-client-match" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-statuslog" edge="1" parent="region-client" source="clients" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-status-logs">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="620" y="140" />
|
||||||
|
<mxPoint x="620" y="638" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-statuslog-lbl" connectable="0" parent="e-client-statuslog" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-fav" edge="1" parent="region-client" source="clients" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-fav-folders">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="530" y="170" />
|
||||||
|
<mxPoint x="530" y="1045" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-fav-lbl" connectable="0" parent="e-client-fav" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-fav-items" edge="1" parent="region-client" source="client-fav-folders" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-folder-items">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-fav-items-lbl" connectable="0" parent="e-fav-items" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-prop" edge="1" parent="1" source="complexes" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=0;endArrow=ERmany;startArrow=ERone;fontSize=9;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" target="properties">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-prop-lbl" connectable="0" parent="e-complex-prop" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N complex_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-prop" edge="1" parent="1" source="buildings" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="properties">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="215" y="1390" />
|
||||||
|
<mxPoint x="1020" y="1390" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-prop-lbl" connectable="0" parent="e-bldg-prop" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N building_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-room-prop" edge="1" parent="1" source="room-units" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="properties">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="575" y="1710" />
|
||||||
|
<mxPoint x="1060" y="1710" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-room-prop-lbl" connectable="0" parent="e-room-prop" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" value="1:N room_unit_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-prop" edge="1" parent="1" source="staff" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="properties">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="-530" y="448" />
|
||||||
|
<mxPoint x="-530" y="300" />
|
||||||
|
<mxPoint x="380" y="300" />
|
||||||
|
<mxPoint x="380" y="400" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-prop-lbl" connectable="0" parent="e-staff-prop" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" value="agent_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-client" edge="1" parent="1" source="staff" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="clients">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="700" y="490" />
|
||||||
|
<mxPoint x="700" y="300" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-client-lbl" connectable="0" parent="e-staff-client" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" value="agent_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-viewing" edge="1" parent="1" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-viewings">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="2340" y="530" />
|
||||||
|
<mxPoint x="2340" y="530" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-viewing-lbl" connectable="0" parent="e-prop-viewing" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" value="property_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-match" edge="1" parent="1" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-matches">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="2690" y="400" />
|
||||||
|
<mxPoint x="2690" y="990" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-match-lbl" connectable="0" parent="e-prop-match" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" value="property_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-folder" edge="1" parent="1" source="properties" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="client-folder-items">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="2800" y="310" />
|
||||||
|
<mxPoint x="2800" y="905" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-folder-lbl" connectable="0" parent="e-prop-folder" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" value="property_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="region-public" parent="1" style="swimlane;startSize=36;fillColor=#0c1a2e;strokeColor=#7dd3fc;fontColor=#7dd3fc;fontSize=13;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=2;" value="PUBLIC SCHEMA(平台运营层)" vertex="1">
|
||||||
|
<mxGeometry height="560" width="3400" x="-860" y="-1200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-tenants" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.tenants</b>
<hr/>
🔑 PK id: uuid
schema_name: varchar(63) [UNIQUE, immutable]
name: varchar(255)
short_name: varchar(100)
contact_name / contact_email
region: varchar(100)
plan: basic/professional/enterprise
status: creating/active/suspended/
 pending_delete/deleted/failed
suspended_until: timestamptz
suspended_reason: varchar(50)
deleted_at: timestamptz
paid_until: date
on_trial: bool
is_canary: bool
created_at / updated_at: timestamptz
created_by: uuid
extra: jsonb" vertex="1">
|
||||||
|
<mxGeometry height="310" width="290" x="30" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-domains" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.domains</b>
<hr/>
🔑 PK id: uuid
🔗 FK tenant_id → tenants
domain: varchar(253) [UNIQUE]
is_primary: bool [UNIQUE partial]
created_at: timestamptz
⚠ domain 创建后不可修改" vertex="1">
|
||||||
|
<mxGeometry height="135" width="250" x="340" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-tenant-status-logs" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.tenant_status_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK tenant_id → tenants
from_status: varchar(20) [NULL=初始]
to_status: varchar(20)
reason: text
operator_id: uuid [NULL=Celery]
operator_name: varchar(100) [快照]
created_at: timestamptz
⚠ append-only — NO UPDATE/DELETE" vertex="1">
|
||||||
|
<mxGeometry height="175" width="270" x="340" y="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-admins" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.platform_admins</b>
<hr/>
🔑 PK id: uuid
username: varchar(150) [UNIQUE]
email: varchar(254) [UNIQUE]
display_name: varchar(100)
password_hash: varchar(255)
role: super_admin/ops_operator/
 read_only_auditor
is_active: bool
mfa_enabled: bool [TOTP 确认后→TRUE]
last_login_at: timestamptz
created_at / updated_at: timestamptz
🔗 FK created_by → platform_admins" vertex="1">
|
||||||
|
<mxGeometry height="225" width="270" x="640" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-mfa" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.admin_mfa_devices</b>
<hr/>
🔑 PK id: uuid
🔗 FK admin_id → platform_admins
device_name: varchar(100)
totp_secret: varchar(255) [加密]
is_confirmed: bool
created_at: timestamptz
last_used_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="150" width="250" x="930" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-sessions" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.admin_sessions</b>
<hr/>
🔑 PK id: uuid
🔗 FK admin_id → platform_admins
session_token: varchar(255) [UNIQUE]
ip_address: inet
user_agent: text
is_active: bool
created_at: timestamptz
expires_at: timestamptz [30min rolling]
revoked_at: timestamptz
🔗 FK revoked_by → platform_admins" vertex="1">
|
||||||
|
<mxGeometry height="190" width="255" x="930" y="220" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-ip" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.ip_whitelist</b>
<hr/>
🔑 PK id: uuid
cidr: cidr [如 203.0.113.0/24]
label: varchar(100)
is_active: bool
created_at: timestamptz
🔗 FK created_by → platform_admins" vertex="1">
|
||||||
|
<mxGeometry height="135" width="240" x="640" y="295" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-audit" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.platform_audit_logs</b>
<hr/>
🔑 PK id: uuid
operator_id: uuid [NULL=系统]
operator_name: varchar(100) [快照]
action_type: varchar(50)
target_type: varchar(30)
target_id / target_name: varchar
payload_summary: text
result: SUCCESS/FAILED
error_message: text
ip_address: inet
created_at: timestamptz
⚠ append-only — 建议月度 RANGE 分区" vertex="1">
|
||||||
|
<mxGeometry height="225" width="265" x="1210" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-bk-schedules" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.backup_schedules</b>
<hr/>
🔑 PK id: uuid
🔗 FK tenant_id → tenants [NULL=全局]
frequency: hourly/daily/weekly
scheduled_time: time [UTC]
retention_count: int
storage_target: local/s3/r2/gcs
is_active: bool
created_at / updated_at: timestamptz
🔗 FK created_by → platform_admins
UNIQUE(tenant_id)" vertex="1">
|
||||||
|
<mxGeometry height="195" width="265" x="1510" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-bk-records" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.backup_records</b>
<hr/>
🔑 PK id: uuid
🔗 FK tenant_id → tenants
trigger_type: auto/manual/
 pre_upgrade/pre_restore
status: pending/in_progress/
 success/failed
storage_target / storage_path: text
size_bytes: bigint
started_at / completed_at
error_message: text
🔗 FK triggered_by → platform_admins
upgrade_event_id: uuid
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="245" width="265" x="1510" y="270" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-exports" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.export_tasks</b>
<hr/>
🔑 PK id: uuid
🔗 FK tenant_id → tenants
🔗 FK requested_by → platform_admins
modules: text[]
format: csv/json/sql_dump
status: pending/in_progress/done/failed
storage_path / download_url: text
expires_at: timestamptz [24h]
size_bytes: bigint
started_at / completed_at
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="215" width="265" x="1210" y="300" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-versions" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.system_versions</b>
<hr/>
🔑 PK id: uuid
version_number: varchar(50) [UNIQUE]
release_notes: text
artifact_url: text
status: current/previous/archived
 [UNIQUE partial: status='current']
released_at: timestamptz
🔗 FK created_by → platform_admins" vertex="1">
|
||||||
|
<mxGeometry height="165" width="265" x="1810" y="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pub-upgrades" parent="region-public" style="text;html=1;strokeColor=#7dd3fc;fillColor=#0c1a2e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=10;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" value="<b>public.upgrade_events</b>
<hr/>
🔑 PK id: uuid
🔗 FK from_version_id → system_versions
🔗 FK to_version_id → system_versions
event_type: upgrade/rollback
strategy: full/canary
status: pending/health_check/
 in_progress/success/failed/rolled_back
tenant_progress: jsonb [{tenant,status,...}]
rollback_reason: text
incident_report: text
started_at / completed_at
🔗 FK initiated_by → platform_admins
created_at: timestamptz" vertex="1">
|
||||||
|
<mxGeometry height="255" width="280" x="1810" y="240" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-domain" edge="1" parent="region-public" source="pub-tenants" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-domains">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-domain-lbl" connectable="0" parent="pe-tenant-domain" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-statuslog" edge="1" parent="region-public" source="pub-tenants" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-tenant-status-logs">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="320" y="295" />
|
||||||
|
<mxPoint x="320" y="288" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-statuslog-lbl" connectable="0" parent="pe-tenant-statuslog" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N append-only" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-admin-mfa" edge="1" parent="region-public" source="pub-admins" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-mfa">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-admin-mfa-lbl" connectable="0" parent="pe-admin-mfa" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-admin-session" edge="1" parent="region-public" source="pub-admins" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-sessions">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="905" y="235" />
|
||||||
|
<mxPoint x="905" y="315" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-admin-session-lbl" connectable="0" parent="pe-admin-session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-admin-ip" edge="1" parent="region-public" source="pub-admins" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-ip">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="755" y="280" />
|
||||||
|
<mxPoint x="755" y="363" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-admin-ip-lbl" connectable="0" parent="pe-admin-ip" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="created_by" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-bksched" edge="1" parent="region-public" source="pub-tenants" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-bk-schedules">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="320" y="430" />
|
||||||
|
<mxPoint x="1642" y="430" />
|
||||||
|
<mxPoint x="1642" y="248" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-bksched-lbl" connectable="0" parent="pe-tenant-bksched" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N (NULL=全局)" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-bkrec" edge="1" parent="region-public" source="pub-tenants" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-bk-records">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="320" y="480" />
|
||||||
|
<mxPoint x="1642" y="480" />
|
||||||
|
<mxPoint x="1642" y="395" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-bkrec-lbl" connectable="0" parent="pe-tenant-bkrec" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-export" edge="1" parent="region-public" source="pub-tenants" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-exports">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="320" y="520" />
|
||||||
|
<mxPoint x="1342" y="520" />
|
||||||
|
<mxPoint x="1342" y="408" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-tenant-export-lbl" connectable="0" parent="pe-tenant-export" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="1:N" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-ver-upgrade" edge="1" parent="region-public" source="pub-versions" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-upgrades">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-ver-upgrade-lbl" connectable="0" parent="pe-ver-upgrade" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="from/to version" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-upgrade-bkrec" edge="1" parent="region-public" source="pub-upgrades" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#7dd3fc;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" target="pub-bk-records">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1808" y="392" />
|
||||||
|
<mxPoint x="1775" y="392" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="pe-upgrade-bkrec-lbl" connectable="0" parent="pe-upgrade-bkrec" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#7dd3fc;" value="upgrade_event_id" vertex="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
774
Project/fonrey/DATA_MODEL/diagram/fonrey-data-model.xml.bak
Normal file
@@ -0,0 +1,774 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2026-04-24" agent="OpenCode" version="21.0.0">
|
||||||
|
<diagram name="Fonrey ER Diagram" id="fonrey-er-v1">
|
||||||
|
<mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="3300" pageHeight="2340" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- SWIM LANE BACKGROUNDS -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- ORG / HR region -->
|
||||||
|
<mxCell id="region-org" value="ORG / HR" style="swimlane;startSize=30;fillColor=#0d3349;strokeColor=#22d3ee;fontColor=#22d3ee;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="60" width="340" height="760" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- REGION & COMPLEX region -->
|
||||||
|
<mxCell id="region-complex" value="REGION & COMPLEX" style="swimlane;startSize=30;fillColor=#063b2f;strokeColor=#34d399;fontColor=#34d399;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="60" width="820" height="1380" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- PROPERTY region -->
|
||||||
|
<mxCell id="region-property" value="PROPERTY" style="swimlane;startSize=30;fillColor=#2d1a5e;strokeColor=#a78bfa;fontColor=#a78bfa;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1280" y="60" width="900" height="1700" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- CLIENT region -->
|
||||||
|
<mxCell id="region-client" value="CLIENT" style="swimlane;startSize=30;fillColor=#3d1f06;strokeColor=#fbbf24;fontColor=#fbbf24;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2220" y="60" width="860" height="1380" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- ORG MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- org_units -->
|
||||||
|
<mxCell id="org-units" value="<b>org_units</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
parent_id: uuid (FK → self)
|
||||||
|
type: varchar(20)
|
||||||
|
name: varchar(100)
|
||||||
|
path: varchar(500) [物化路径]
|
||||||
|
depth: smallint
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
|
||||||
|
<mxGeometry x="30" y="60" width="280" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- staff -->
|
||||||
|
<mxCell id="staff" value="<b>staff</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
FK org_unit_id → org_units
|
||||||
|
name: varchar(50)
|
||||||
|
phone_enc: text [AES-256-GCM]
|
||||||
|
phone_hash: varchar(64) [SHA-256]
|
||||||
|
id_no_enc: text [AES]
|
||||||
|
user_id: uuid [FK → auth_user]
|
||||||
|
entry_date: date
|
||||||
|
status: active/resigned/...
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
|
||||||
|
<mxGeometry x="30" y="310" width="280" height="215" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- REGION & COMPLEX MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- districts -->
|
||||||
|
<mxCell id="districts" value="<b>districts</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
city: varchar(50)
|
||||||
|
name: varchar(50)
|
||||||
|
short_name: varchar(20)
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="60" width="280" height="150" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- business_areas -->
|
||||||
|
<mxCell id="business-areas" value="<b>business_areas</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK district_id → districts
|
||||||
|
name: varchar(100)
|
||||||
|
latitude: numeric(10,7)
|
||||||
|
longitude: numeric(10,7)
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="310" width="280" height="155" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- schools -->
|
||||||
|
<mxCell id="schools" value="<b>schools</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK district_id → districts
|
||||||
|
name: varchar(100)
|
||||||
|
type: primary/middle/high/k9/k12
|
||||||
|
nature: public/private/international
|
||||||
|
level: normal/key/top
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="60" width="290" height="155" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complexes -->
|
||||||
|
<mxCell id="complexes" value="<b>complexes</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK district_id → districts
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
name: varchar(200) [⚠ 不可直接修改]
|
||||||
|
address: varchar(500) [只读]
|
||||||
|
address_summary: varchar(100)
|
||||||
|
latitude: numeric(10,7)
|
||||||
|
longitude: numeric(10,7)
|
||||||
|
property_usage_types: varchar[]
|
||||||
|
building_structure: varchar(30)
|
||||||
|
building_type: slab/tower/slab_tower
|
||||||
|
land_use_years: varchar(30)
|
||||||
|
built_years: smallint[]
|
||||||
|
total_units: int
|
||||||
|
total_households: int
|
||||||
|
total_floor_area: numeric(12,2)
|
||||||
|
plot_area: numeric(12,2)
|
||||||
|
plot_ratio: numeric(5,2)
|
||||||
|
green_rate: numeric(5,2)
|
||||||
|
developer: varchar(200)
|
||||||
|
property_company: varchar(200)
|
||||||
|
property_fee: numeric(8,2)
|
||||||
|
property_phone: varchar(30)
|
||||||
|
parking_total: int
|
||||||
|
parking_underground: int
|
||||||
|
parking_ratio: varchar(20)
|
||||||
|
water_type: civil/commercial
|
||||||
|
electricity_type: civil/commercial
|
||||||
|
has_central_heating: bool
|
||||||
|
has_gas: bool
|
||||||
|
lock_building: bool
|
||||||
|
lock_room: bool
|
||||||
|
lock_info: bool
|
||||||
|
lock_standard_room: bool
|
||||||
|
search_vector: tsvector
|
||||||
|
remarks: text
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
updated_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="570" width="340" height="570" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complex_aliases -->
|
||||||
|
<mxCell id="complex-aliases" value="<b>complex_aliases</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
alias: varchar(200)
|
||||||
|
is_system: bool [系统别名只读]
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="570" width="290" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complex_business_areas (join) -->
|
||||||
|
<mxCell id="complex-biz-areas" value="<b>complex_business_areas</b> [N:M join]
|
||||||
|
<hr/>
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK business_area_id → business_areas
|
||||||
|
is_primary: bool [UNIQUE where TRUE]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="490" width="370" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complex_schools (join) -->
|
||||||
|
<mxCell id="complex-schools" value="<b>complex_schools</b> [N:M join]
|
||||||
|
<hr/>
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK school_id → schools
|
||||||
|
zone_type: guaranteed/reference/lottery" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="250" width="300" height="75" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- buildings -->
|
||||||
|
<mxCell id="buildings" value="<b>buildings</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK school_id → schools [楼栋级学区]
|
||||||
|
name: varchar(50)
|
||||||
|
is_standard: bool
|
||||||
|
property_usage_type: varchar(20)
|
||||||
|
built_year: smallint
|
||||||
|
total_floors: smallint
|
||||||
|
land_use_years: varchar(30)
|
||||||
|
has_elevator: bool
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="1000" width="310" height="225" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- room_units -->
|
||||||
|
<mxCell id="room-units" value="<b>room_units</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK building_id → buildings
|
||||||
|
floor: smallint
|
||||||
|
floor_name: varchar(20)
|
||||||
|
room_no: varchar(30)
|
||||||
|
display_no: varchar(50)
|
||||||
|
is_standard: bool
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
updated_at: timestamptz
|
||||||
|
UNIQUE(building_id, floor, room_no)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="1260" width="310" height="200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complex_price_trends -->
|
||||||
|
<mxCell id="complex-price-trends" value="<b>complex_price_trends</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
record_month: date [存月份1日]
|
||||||
|
avg_sale_price: numeric(12,2)
|
||||||
|
avg_unit_price: numeric(10,2)
|
||||||
|
transaction_count: int
|
||||||
|
listing_count: int
|
||||||
|
created_at: timestamptz
|
||||||
|
UNIQUE(complex_id, record_month)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="400" y="1000" width="380" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- metro_lines -->
|
||||||
|
<mxCell id="metro-lines" value="<b>metro_lines</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
city: varchar(50)
|
||||||
|
name: varchar(50)
|
||||||
|
color: varchar(7) [HEX]
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="1520" width="260" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- metro_stations -->
|
||||||
|
<mxCell id="metro-stations" value="<b>metro_stations</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK metro_line_id → metro_lines
|
||||||
|
name: varchar(50)
|
||||||
|
latitude: numeric(10,7)
|
||||||
|
longitude: numeric(10,7)
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="320" y="1520" width="280" height="150" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complex_metro_stations (join) -->
|
||||||
|
<mxCell id="complex-metro-stations" value="<b>complex_metro_stations</b> [N:M join]
|
||||||
|
<hr/>
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK station_id → metro_stations
|
||||||
|
distance_meters: int [步行距离]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="320" y="1700" width="320" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- complex_photos -->
|
||||||
|
<mxCell id="complex-photos" value="<b>complex_photos</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
category: complex/layout/vr/other
|
||||||
|
file_key: text [R2/S3]
|
||||||
|
thumbnail_key: text
|
||||||
|
file_name: varchar(255)
|
||||||
|
file_size: int
|
||||||
|
width, height: int
|
||||||
|
is_cover: bool [UNIQUE where TRUE]
|
||||||
|
sort_order: smallint
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="770" width="300" height="205" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- PROPERTY MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- properties -->
|
||||||
|
<mxCell id="properties" value="<b>properties</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK building_id → buildings
|
||||||
|
🔗 FK room_unit_id → room_units
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
listing_type: sale/rent/both
|
||||||
|
status: varchar(20)
|
||||||
|
sale_price: numeric(12,2) [万元]
|
||||||
|
rent_price: numeric(10,2) [元/月]
|
||||||
|
area: numeric(8,2) [m²]
|
||||||
|
floor: smallint
|
||||||
|
total_floors: smallint
|
||||||
|
bedroom: smallint
|
||||||
|
living_room: smallint
|
||||||
|
bathroom: smallint
|
||||||
|
orientation: varchar(30)
|
||||||
|
decoration: varchar(20)
|
||||||
|
has_elevator: bool
|
||||||
|
built_year: smallint
|
||||||
|
ownership_years: varchar(20)
|
||||||
|
is_exclusive: bool [独家委托]
|
||||||
|
completeness_score: int
|
||||||
|
search_vector: tsvector
|
||||||
|
source: varchar(30)
|
||||||
|
remarks: text
|
||||||
|
created_at: timestamptz
|
||||||
|
updated_at: timestamptz
|
||||||
|
deleted_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
🔗 FK updated_by → staff
|
||||||
|
<i>[89,000+ rows · 复合索引 · 分区预留]</i>" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="60" width="380" height="560" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_contacts -->
|
||||||
|
<mxCell id="property-contacts" value="<b>property_contacts</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
name: varchar(50)
|
||||||
|
phone_enc: text [AES-256-GCM]
|
||||||
|
phone_hash: varchar(64) [SHA-256]
|
||||||
|
role: owner/agent/tenant
|
||||||
|
is_primary: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="670" width="310" height="170" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_follow_logs -->
|
||||||
|
<mxCell id="property-follow-logs" value="<b>property_follow_logs</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK staff_id → staff
|
||||||
|
log_type: call/visit/price_change/note/...
|
||||||
|
content: text
|
||||||
|
phone_no_viewed: bool [敏感操作]
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="60" width="380" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- listing_histories -->
|
||||||
|
<mxCell id="listing-histories" value="<b>listing_histories</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
listed_at: timestamptz
|
||||||
|
delisted_at: timestamptz
|
||||||
|
list_price: numeric(12,2)
|
||||||
|
reason: varchar(50)
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="300" width="310" height="155" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_photos -->
|
||||||
|
<mxCell id="property-photos" value="<b>property_photos</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
category: listing/vr/layout/other
|
||||||
|
file_key: text [R2/S3]
|
||||||
|
thumbnail_key: text
|
||||||
|
is_cover: bool
|
||||||
|
sort_order: smallint
|
||||||
|
width, height: int
|
||||||
|
file_size: int
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="900" width="310" height="205" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_keys -->
|
||||||
|
<mxCell id="property-keys" value="<b>property_keys</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK holder_id → staff
|
||||||
|
key_no: varchar(50)
|
||||||
|
status: held/returned
|
||||||
|
taken_at: timestamptz
|
||||||
|
returned_at: timestamptz
|
||||||
|
notes: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="510" width="310" height="165" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_commissions -->
|
||||||
|
<mxCell id="property-commissions" value="<b>property_commissions</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
commission_type: exclusive/open
|
||||||
|
rate: numeric(5,4)
|
||||||
|
amount: numeric(12,2)
|
||||||
|
start_date: date
|
||||||
|
end_date: date
|
||||||
|
signed_at: timestamptz
|
||||||
|
document_key: text
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="740" width="330" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_inspections -->
|
||||||
|
<mxCell id="property-inspections" value="<b>property_inspections</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK staff_id → staff
|
||||||
|
inspected_at: timestamptz
|
||||||
|
status: pending/done/cancelled
|
||||||
|
notes: text
|
||||||
|
attachments: jsonb
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="1160" width="320" height="165" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_marketing -->
|
||||||
|
<mxCell id="property-marketing" value="<b>property_marketing</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties [UNIQUE 1:1]
|
||||||
|
title: varchar(200)
|
||||||
|
highlights: text[]
|
||||||
|
description: text
|
||||||
|
tags: varchar[]
|
||||||
|
platforms: jsonb
|
||||||
|
published_at: timestamptz
|
||||||
|
updated_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="990" width="340" height="175" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- property_certificates -->
|
||||||
|
<mxCell id="property-certificates" value="<b>property_certificates</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties [UNIQUE 1:1]
|
||||||
|
cert_no: varchar(50)
|
||||||
|
owner_name: varchar(100)
|
||||||
|
ownership_type: varchar(30)
|
||||||
|
area_registered: numeric(8,2)
|
||||||
|
issue_date: date
|
||||||
|
document_key: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="1390" width="330" height="165" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- completeness_scores -->
|
||||||
|
<mxCell id="completeness-scores" value="<b>completeness_scores</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties [UNIQUE 1:1]
|
||||||
|
score: int [0-100]
|
||||||
|
missing_fields: text[]
|
||||||
|
calculated_at: timestamptz
|
||||||
|
version: int" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="1230" width="310" height="135" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- CLIENT MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- clients -->
|
||||||
|
<mxCell id="clients" value="<b>clients</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
client_type: private/public/closed
|
||||||
|
status: active/inactive/converted
|
||||||
|
name: varchar(50)
|
||||||
|
phone_enc: text [AES-256-GCM]
|
||||||
|
phone_hash: varchar(64) [SHA-256]
|
||||||
|
budget_min/max: numeric
|
||||||
|
activity_level: 1-5 [Celery每日计算]
|
||||||
|
is_protected: bool [防自动转公客]
|
||||||
|
transfer_to_public_type: auto/manual
|
||||||
|
last_follow_at: timestamptz
|
||||||
|
source: varchar(30)
|
||||||
|
remarks: text
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
<i>[私客/公客/成交客 三态状态机]</i>" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="60" width="370" height="360" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_requirements -->
|
||||||
|
<mxCell id="client-requirements" value="<b>client_requirements</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
req_type: second_hand/new/rent
|
||||||
|
district_ids: uuid[]
|
||||||
|
business_area_ids: uuid[]
|
||||||
|
price_min: numeric
|
||||||
|
price_max: numeric
|
||||||
|
area_min: numeric
|
||||||
|
area_max: numeric
|
||||||
|
bedrooms: int[]
|
||||||
|
school_ids: uuid[]
|
||||||
|
has_elevator: bool
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="480" width="350" height="260" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_follow_logs -->
|
||||||
|
<mxCell id="client-follow-logs" value="<b>client_follow_logs</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
🔗 FK staff_id → staff
|
||||||
|
log_type: call/visit/match/note/status_change
|
||||||
|
content: text
|
||||||
|
next_follow_date: date
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="430" y="60" width="380" height="200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_viewings -->
|
||||||
|
<mxCell id="client-viewings" value="<b>client_viewings</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
viewed_at: timestamptz
|
||||||
|
feedback: text
|
||||||
|
rating: smallint [1-5]
|
||||||
|
status: planned/done/cancelled
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="430" y="310" width="360" height="195" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_property_matches -->
|
||||||
|
<mxCell id="client-matches" value="<b>client_property_matches</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
match_type: system/manual
|
||||||
|
score: numeric(5,2)
|
||||||
|
status: pending/sent/viewed/dismissed
|
||||||
|
sent_at: timestamptz
|
||||||
|
viewed_at: timestamptz
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="800" width="380" height="205" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_status_logs -->
|
||||||
|
<mxCell id="client-status-logs" value="<b>client_status_logs</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
from_status: varchar(20)
|
||||||
|
to_status: varchar(20)
|
||||||
|
transfer_type: auto/manual
|
||||||
|
reason: text
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="430" y="560" width="370" height="195" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_favorite_folders -->
|
||||||
|
<mxCell id="client-fav-folders" value="<b>client_favorite_folders</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
name: varchar(100)
|
||||||
|
sort_order: int
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="1070" width="300" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- client_folder_items -->
|
||||||
|
<mxCell id="client-folder-items" value="<b>client_folder_items</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK folder_id → client_favorite_folders
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
sort_order: int
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="370" y="1070" width="320" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- EDGES / RELATIONSHIPS -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- OrgUnit self-ref -->
|
||||||
|
<mxCell id="e-org-self" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="org-units" parent="region-org">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="340" y="153"/><mxPoint x="340" y="108"/></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-self-lbl" value="自引用 parent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-self"><mxGeometry x="0.1" relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- OrgUnit → Staff -->
|
||||||
|
<mxCell id="e-org-staff" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="staff" parent="region-org">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-staff-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-staff"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- District → BusinessArea -->
|
||||||
|
<mxCell id="e-dist-biz" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="business-areas" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-biz-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-biz"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- District → Schools -->
|
||||||
|
<mxCell id="e-dist-school" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="schools" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-school-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-school"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- District → Complexes -->
|
||||||
|
<mxCell id="e-dist-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-complex-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-complex"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- BusinessArea ↔ Complexes via join -->
|
||||||
|
<mxCell id="e-biz-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="business-areas" target="complex-biz-areas" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-join-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-biz-areas" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Schools ↔ Complexes via join -->
|
||||||
|
<mxCell id="e-school-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="schools" target="complex-schools" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-school-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-schools" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Complexes → complex_aliases -->
|
||||||
|
<mxCell id="e-complex-alias" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-aliases" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-alias-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-alias"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Complexes → complex_photos -->
|
||||||
|
<mxCell id="e-complex-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-photos" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Complexes → complex_price_trends -->
|
||||||
|
<mxCell id="e-complex-trend" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-price-trends" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-trend-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-trend"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Complexes → Buildings -->
|
||||||
|
<mxCell id="e-complex-bldg" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="buildings" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-bldg-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-bldg"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Buildings → RoomUnits -->
|
||||||
|
<mxCell id="e-bldg-room" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="room-units" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-room-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-bldg-room"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- MetroLine → MetroStation -->
|
||||||
|
<mxCell id="e-metro-line-station" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="metro-lines" target="metro-stations" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-metro-line-station"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- MetroStation ↔ Complexes via join -->
|
||||||
|
<mxCell id="e-metro-join1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="metro-stations" target="complex-metro-stations" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-metro-stations" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Properties → PropertyContacts -->
|
||||||
|
<mxCell id="e-prop-contact" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-contacts" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-contact-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-contact"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → FollowLogs -->
|
||||||
|
<mxCell id="e-prop-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-follow-logs" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → ListingHistories -->
|
||||||
|
<mxCell id="e-prop-listing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="listing-histories" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-listing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-listing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Photos -->
|
||||||
|
<mxCell id="e-prop-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-photos" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Keys -->
|
||||||
|
<mxCell id="e-prop-keys" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-keys" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-keys-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-keys"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Commissions -->
|
||||||
|
<mxCell id="e-prop-comm" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-commissions" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-comm-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-comm"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Inspections -->
|
||||||
|
<mxCell id="e-prop-insp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-inspections" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-insp-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-insp"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Marketing (1:1) -->
|
||||||
|
<mxCell id="e-prop-marketing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-marketing" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-marketing-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-marketing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Certificates (1:1) -->
|
||||||
|
<mxCell id="e-prop-cert" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-certificates" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-cert-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-cert"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Completeness (1:1) -->
|
||||||
|
<mxCell id="e-prop-score" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="completeness-scores" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-score-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-score"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Clients → ClientRequirements -->
|
||||||
|
<mxCell id="e-client-req" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-requirements" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-req-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-req"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Clients → FollowLogs -->
|
||||||
|
<mxCell id="e-client-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-follow-logs" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Clients → Viewings -->
|
||||||
|
<mxCell id="e-client-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-viewings" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-viewing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Clients → Matches -->
|
||||||
|
<mxCell id="e-client-match" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-matches" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-match-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-match"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Clients → StatusLogs -->
|
||||||
|
<mxCell id="e-client-statuslog" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-status-logs" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-statuslog-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-statuslog"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Clients → FavFolders -->
|
||||||
|
<mxCell id="e-client-fav" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-fav-folders" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-fav-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-fav"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- FavFolders → FolderItems -->
|
||||||
|
<mxCell id="e-fav-items" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="client-fav-folders" target="client-folder-items" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-fav-items-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-fav-items"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- CROSS-REGION EDGES (parent=1) -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- Complexes → Properties -->
|
||||||
|
<mxCell id="e-complex-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=0;endArrow=ERmany;startArrow=ERone;fontSize=9;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" source="complexes" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-prop-lbl" value="1:N complex_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-complex-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Buildings → Properties -->
|
||||||
|
<mxCell id="e-bldg-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-prop-lbl" value="1:N building_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-bldg-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- RoomUnits → Properties -->
|
||||||
|
<mxCell id="e-room-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="room-units" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-room-prop-lbl" value="1:N room_unit_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-room-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Staff → Properties (agent_id) -->
|
||||||
|
<mxCell id="e-staff-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-prop-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Staff → Clients (agent_id) -->
|
||||||
|
<mxCell id="e-staff-client" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="clients" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-client-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-client"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Viewings (cross-region) -->
|
||||||
|
<mxCell id="e-prop-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-viewings" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-viewing-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → Matches (cross-region) -->
|
||||||
|
<mxCell id="e-prop-match" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-matches" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-match-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-match"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
<!-- Properties → FolderItems (cross-region) -->
|
||||||
|
<mxCell id="e-prop-folder" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-folder-items" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-folder-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-folder"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
860
Project/fonrey/DATA_MODEL/diagram/fonrey-er.drawio
Normal file
@@ -0,0 +1,860 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2026-04-24" agent="OpenCode" version="21.0.0">
|
||||||
|
<diagram name="Fonrey ER Diagram" id="fonrey-er-v1">
|
||||||
|
<mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="3300" pageHeight="2340" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- SWIM LANE BACKGROUNDS -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- ORG / HR region -->
|
||||||
|
<mxCell id="region-org" value="ORG / HR" style="swimlane;startSize=30;fillColor=#0d3349;strokeColor=#22d3ee;fontColor=#22d3ee;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="60" width="340" height="760" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- REGION & COMPLEX region -->
|
||||||
|
<mxCell id="region-complex" value="REGION & COMPLEX" style="swimlane;startSize=30;fillColor=#063b2f;strokeColor=#34d399;fontColor=#34d399;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="60" width="820" height="1380" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- PROPERTY region -->
|
||||||
|
<mxCell id="region-property" value="PROPERTY" style="swimlane;startSize=30;fillColor=#2d1a5e;strokeColor=#a78bfa;fontColor=#a78bfa;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1280" y="60" width="900" height="1700" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- CLIENT region -->
|
||||||
|
<mxCell id="region-client" value="CLIENT" style="swimlane;startSize=30;fillColor=#3d1f06;strokeColor=#fbbf24;fontColor=#fbbf24;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2220" y="60" width="860" height="1380" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- ORG MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- org_units -->
|
||||||
|
<mxCell id="org-units" value="<b>org_units</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
parent_id: uuid (FK → self)
|
||||||
|
type: varchar(20)
|
||||||
|
name: varchar(100)
|
||||||
|
path: varchar(500) [物化路径]
|
||||||
|
depth: smallint
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
|
||||||
|
<mxGeometry x="30" y="60" width="280" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- staff -->
|
||||||
|
<mxCell id="staff" value="<b>staff</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
FK org_unit_id → org_units
|
||||||
|
name: varchar(50)
|
||||||
|
phone_enc: text [AES-256-GCM]
|
||||||
|
phone_hash: varchar(64) [SHA-256]
|
||||||
|
id_no_enc: text [AES]
|
||||||
|
user_id: uuid [FK → auth_user]
|
||||||
|
entry_date: date
|
||||||
|
status: active/resigned/...
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
|
||||||
|
<mxGeometry x="30" y="310" width="280" height="215" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- REGION & COMPLEX MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- districts -->
|
||||||
|
<mxCell id="districts" value="<b>districts</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
city: varchar(50)
|
||||||
|
name: varchar(50)
|
||||||
|
short_name: varchar(20)
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="60" width="280" height="150" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- business_areas -->
|
||||||
|
<mxCell id="business-areas" value="<b>business_areas</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK district_id → districts
|
||||||
|
name: varchar(100)
|
||||||
|
latitude: numeric(10,7)
|
||||||
|
longitude: numeric(10,7)
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="310" width="280" height="155" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- schools -->
|
||||||
|
<mxCell id="schools" value="<b>schools</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK district_id → districts
|
||||||
|
name: varchar(100)
|
||||||
|
type: primary/middle/high/k9/k12
|
||||||
|
nature: public/private/international
|
||||||
|
level: normal/key/top
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="60" width="290" height="155" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complexes -->
|
||||||
|
<mxCell id="complexes" value="<b>complexes</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK district_id → districts
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
name: varchar(200) [⚠ 不可直接修改]
|
||||||
|
address: varchar(500) [只读]
|
||||||
|
address_summary: varchar(100)
|
||||||
|
latitude: numeric(10,7)
|
||||||
|
longitude: numeric(10,7)
|
||||||
|
property_usage_types: varchar[]
|
||||||
|
building_structure: varchar(30)
|
||||||
|
building_type: slab/tower/slab_tower
|
||||||
|
land_use_years: varchar(30)
|
||||||
|
built_years: smallint[]
|
||||||
|
total_units: int
|
||||||
|
total_households: int
|
||||||
|
total_floor_area: numeric(12,2)
|
||||||
|
plot_area: numeric(12,2)
|
||||||
|
plot_ratio: numeric(5,2)
|
||||||
|
green_rate: numeric(5,2)
|
||||||
|
developer: varchar(200)
|
||||||
|
property_company: varchar(200)
|
||||||
|
property_fee: numeric(8,2)
|
||||||
|
property_phone: varchar(30)
|
||||||
|
parking_total: int
|
||||||
|
parking_underground: int
|
||||||
|
parking_ratio: varchar(20)
|
||||||
|
water_type: civil/commercial
|
||||||
|
electricity_type: civil/commercial
|
||||||
|
has_central_heating: bool
|
||||||
|
has_gas: bool
|
||||||
|
lock_building: bool
|
||||||
|
lock_room: bool
|
||||||
|
lock_info: bool
|
||||||
|
lock_standard_room: bool
|
||||||
|
search_vector: tsvector
|
||||||
|
remarks: text
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
updated_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="570" width="340" height="570" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complex_aliases -->
|
||||||
|
<mxCell id="complex-aliases" value="<b>complex_aliases</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
alias: varchar(200)
|
||||||
|
is_system: bool [系统别名只读]
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="570" width="290" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complex_business_areas (join) -->
|
||||||
|
<mxCell id="complex-biz-areas" value="<b>complex_business_areas</b> [N:M join]
|
||||||
|
<hr/>
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK business_area_id → business_areas
|
||||||
|
is_primary: bool [UNIQUE where TRUE]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="490" width="370" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complex_schools (join) -->
|
||||||
|
<mxCell id="complex-schools" value="<b>complex_schools</b> [N:M join]
|
||||||
|
<hr/>
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK school_id → schools
|
||||||
|
zone_type: guaranteed/reference/lottery" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="250" width="300" height="75" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- buildings -->
|
||||||
|
<mxCell id="buildings" value="<b>buildings</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK school_id → schools [楼栋级学区]
|
||||||
|
name: varchar(50)
|
||||||
|
is_standard: bool
|
||||||
|
property_usage_type: varchar(20)
|
||||||
|
built_year: smallint
|
||||||
|
total_floors: smallint
|
||||||
|
land_use_years: varchar(30)
|
||||||
|
has_elevator: bool
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="1000" width="310" height="225" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- room_units -->
|
||||||
|
<mxCell id="room-units" value="<b>room_units</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK building_id → buildings
|
||||||
|
floor: smallint
|
||||||
|
floor_name: varchar(20)
|
||||||
|
room_no: varchar(30)
|
||||||
|
display_no: varchar(50)
|
||||||
|
is_standard: bool
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
updated_at: timestamptz
|
||||||
|
UNIQUE(building_id, floor, room_no)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="1260" width="310" height="200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complex_price_trends -->
|
||||||
|
<mxCell id="complex-price-trends" value="<b>complex_price_trends</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
record_month: date [存月份1日]
|
||||||
|
avg_sale_price: numeric(12,2)
|
||||||
|
avg_unit_price: numeric(10,2)
|
||||||
|
transaction_count: int
|
||||||
|
listing_count: int
|
||||||
|
created_at: timestamptz
|
||||||
|
UNIQUE(complex_id, record_month)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="400" y="1000" width="380" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- metro_lines -->
|
||||||
|
<mxCell id="metro-lines" value="<b>metro_lines</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
city: varchar(50)
|
||||||
|
name: varchar(50)
|
||||||
|
color: varchar(7) [HEX]
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="30" y="1520" width="260" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- metro_stations -->
|
||||||
|
<mxCell id="metro-stations" value="<b>metro_stations</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK metro_line_id → metro_lines
|
||||||
|
name: varchar(50)
|
||||||
|
latitude: numeric(10,7)
|
||||||
|
longitude: numeric(10,7)
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="320" y="1520" width="280" height="150" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complex_metro_stations (join) -->
|
||||||
|
<mxCell id="complex-metro-stations" value="<b>complex_metro_stations</b> [N:M join]
|
||||||
|
<hr/>
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK station_id → metro_stations
|
||||||
|
distance_meters: int [步行距离]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="320" y="1700" width="320" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- complex_photos -->
|
||||||
|
<mxCell id="complex-photos" value="<b>complex_photos</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
category: complex/layout/vr/other
|
||||||
|
file_key: text [R2/S3]
|
||||||
|
thumbnail_key: text
|
||||||
|
file_name: varchar(255)
|
||||||
|
file_size: int
|
||||||
|
width, height: int
|
||||||
|
is_cover: bool [UNIQUE where TRUE]
|
||||||
|
sort_order: smallint
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
|
||||||
|
<mxGeometry x="490" y="770" width="300" height="205" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- PROPERTY MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- properties -->
|
||||||
|
<mxCell id="properties" value="<b>properties</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK complex_id → complexes
|
||||||
|
🔗 FK building_id → buildings
|
||||||
|
🔗 FK room_unit_id → room_units
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
listing_type: sale/rent/both
|
||||||
|
status: varchar(20)
|
||||||
|
sale_price: numeric(12,2) [万元]
|
||||||
|
rent_price: numeric(10,2) [元/月]
|
||||||
|
area: numeric(8,2) [m²]
|
||||||
|
floor: smallint
|
||||||
|
total_floors: smallint
|
||||||
|
bedroom: smallint
|
||||||
|
living_room: smallint
|
||||||
|
bathroom: smallint
|
||||||
|
orientation: varchar(30)
|
||||||
|
decoration: varchar(20)
|
||||||
|
has_elevator: bool
|
||||||
|
built_year: smallint
|
||||||
|
ownership_years: varchar(20)
|
||||||
|
is_exclusive: bool [独家委托]
|
||||||
|
completeness_score: int
|
||||||
|
search_vector: tsvector
|
||||||
|
source: varchar(30)
|
||||||
|
remarks: text
|
||||||
|
created_at: timestamptz
|
||||||
|
updated_at: timestamptz
|
||||||
|
deleted_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
🔗 FK updated_by → staff
|
||||||
|
<i>[89,000+ rows · 复合索引 · 分区预留]</i>" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="60" width="380" height="560" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_contacts -->
|
||||||
|
<mxCell id="property-contacts" value="<b>property_contacts</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
name: varchar(50)
|
||||||
|
phone_enc: text [AES-256-GCM]
|
||||||
|
phone_hash: varchar(64) [SHA-256]
|
||||||
|
role: owner/agent/tenant
|
||||||
|
is_primary: bool
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="670" width="310" height="170" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_follow_logs -->
|
||||||
|
<mxCell id="property-follow-logs" value="<b>property_follow_logs</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK staff_id → staff
|
||||||
|
log_type: call/visit/price_change/note/...
|
||||||
|
content: text
|
||||||
|
phone_no_viewed: bool [敏感操作]
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="60" width="380" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- listing_histories -->
|
||||||
|
<mxCell id="listing-histories" value="<b>listing_histories</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
listed_at: timestamptz
|
||||||
|
delisted_at: timestamptz
|
||||||
|
list_price: numeric(12,2)
|
||||||
|
reason: varchar(50)
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="300" width="310" height="155" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_photos -->
|
||||||
|
<mxCell id="property-photos" value="<b>property_photos</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
category: listing/vr/layout/other
|
||||||
|
file_key: text [R2/S3]
|
||||||
|
thumbnail_key: text
|
||||||
|
is_cover: bool
|
||||||
|
sort_order: smallint
|
||||||
|
width, height: int
|
||||||
|
file_size: int
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="900" width="310" height="205" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_keys -->
|
||||||
|
<mxCell id="property-keys" value="<b>property_keys</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK holder_id → staff
|
||||||
|
key_no: varchar(50)
|
||||||
|
status: held/returned
|
||||||
|
taken_at: timestamptz
|
||||||
|
returned_at: timestamptz
|
||||||
|
notes: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="510" width="310" height="165" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_commissions -->
|
||||||
|
<mxCell id="property-commissions" value="<b>property_commissions</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
commission_type: exclusive/open
|
||||||
|
rate: numeric(5,4)
|
||||||
|
amount: numeric(12,2)
|
||||||
|
start_date: date
|
||||||
|
end_date: date
|
||||||
|
signed_at: timestamptz
|
||||||
|
document_key: text
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="740" width="330" height="185" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_inspections -->
|
||||||
|
<mxCell id="property-inspections" value="<b>property_inspections</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK staff_id → staff
|
||||||
|
inspected_at: timestamptz
|
||||||
|
status: pending/done/cancelled
|
||||||
|
notes: text
|
||||||
|
attachments: jsonb
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="1160" width="320" height="165" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_marketing -->
|
||||||
|
<mxCell id="property-marketing" value="<b>property_marketing</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties [UNIQUE 1:1]
|
||||||
|
title: varchar(200)
|
||||||
|
highlights: text[]
|
||||||
|
description: text
|
||||||
|
tags: varchar[]
|
||||||
|
platforms: jsonb
|
||||||
|
published_at: timestamptz
|
||||||
|
updated_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="990" width="340" height="175" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- property_certificates -->
|
||||||
|
<mxCell id="property-certificates" value="<b>property_certificates</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties [UNIQUE 1:1]
|
||||||
|
cert_no: varchar(50)
|
||||||
|
owner_name: varchar(100)
|
||||||
|
ownership_type: varchar(30)
|
||||||
|
area_registered: numeric(8,2)
|
||||||
|
issue_date: date
|
||||||
|
document_key: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="30" y="1390" width="330" height="165" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- completeness_scores -->
|
||||||
|
<mxCell id="completeness-scores" value="<b>completeness_scores</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK property_id → properties [UNIQUE 1:1]
|
||||||
|
score: int [0-100]
|
||||||
|
missing_fields: text[]
|
||||||
|
calculated_at: timestamptz
|
||||||
|
version: int" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
|
||||||
|
<mxGeometry x="470" y="1230" width="310" height="135" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- CLIENT MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- clients -->
|
||||||
|
<mxCell id="clients" value="<b>clients</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
client_type: private/public/closed
|
||||||
|
status: active/inactive/converted
|
||||||
|
name: varchar(50)
|
||||||
|
phone_enc: text [AES-256-GCM]
|
||||||
|
phone_hash: varchar(64) [SHA-256]
|
||||||
|
budget_min/max: numeric
|
||||||
|
activity_level: 1-5 [Celery每日计算]
|
||||||
|
is_protected: bool [防自动转公客]
|
||||||
|
transfer_to_public_type: auto/manual
|
||||||
|
last_follow_at: timestamptz
|
||||||
|
source: varchar(30)
|
||||||
|
remarks: text
|
||||||
|
created_at: timestamptz
|
||||||
|
deleted_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
<i>[私客/公客/成交客 三态状态机]</i>" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="60" width="370" height="360" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_requirements -->
|
||||||
|
<mxCell id="client-requirements" value="<b>client_requirements</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
req_type: second_hand/new/rent
|
||||||
|
district_ids: uuid[]
|
||||||
|
business_area_ids: uuid[]
|
||||||
|
price_min: numeric
|
||||||
|
price_max: numeric
|
||||||
|
area_min: numeric
|
||||||
|
area_max: numeric
|
||||||
|
bedrooms: int[]
|
||||||
|
school_ids: uuid[]
|
||||||
|
has_elevator: bool
|
||||||
|
is_active: bool
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="480" width="350" height="260" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_follow_logs -->
|
||||||
|
<mxCell id="client-follow-logs" value="<b>client_follow_logs</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
🔗 FK staff_id → staff
|
||||||
|
log_type: call/visit/match/note/status_change
|
||||||
|
content: text
|
||||||
|
next_follow_date: date
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="430" y="60" width="380" height="200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_viewings -->
|
||||||
|
<mxCell id="client-viewings" value="<b>client_viewings</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
viewed_at: timestamptz
|
||||||
|
feedback: text
|
||||||
|
rating: smallint [1-5]
|
||||||
|
status: planned/done/cancelled
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="430" y="310" width="360" height="195" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_property_matches -->
|
||||||
|
<mxCell id="client-matches" value="<b>client_property_matches</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
🔗 FK agent_id → staff
|
||||||
|
match_type: system/manual
|
||||||
|
score: numeric(5,2)
|
||||||
|
status: pending/sent/viewed/dismissed
|
||||||
|
sent_at: timestamptz
|
||||||
|
viewed_at: timestamptz
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="800" width="380" height="205" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_status_logs -->
|
||||||
|
<mxCell id="client-status-logs" value="<b>client_status_logs</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
from_status: varchar(20)
|
||||||
|
to_status: varchar(20)
|
||||||
|
transfer_type: auto/manual
|
||||||
|
reason: text
|
||||||
|
created_at: timestamptz
|
||||||
|
🔗 FK created_by → staff
|
||||||
|
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="430" y="560" width="370" height="195" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_favorite_folders -->
|
||||||
|
<mxCell id="client-fav-folders" value="<b>client_favorite_folders</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK client_id → clients
|
||||||
|
name: varchar(100)
|
||||||
|
sort_order: int
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="30" y="1070" width="300" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- client_folder_items -->
|
||||||
|
<mxCell id="client-folder-items" value="<b>client_folder_items</b>
|
||||||
|
<hr/>
|
||||||
|
🔑 PK id: uuid
|
||||||
|
🔗 FK folder_id → client_favorite_folders
|
||||||
|
🔗 FK property_id → properties
|
||||||
|
sort_order: int
|
||||||
|
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
|
||||||
|
<mxGeometry x="370" y="1070" width="320" height="130" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- EDGES / RELATIONSHIPS -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- OrgUnit self-ref -->
|
||||||
|
<mxCell id="e-org-self" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="org-units" parent="region-org">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="340" y="153"/><mxPoint x="340" y="108"/></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-self-lbl" value="自引用 parent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-self"><mxGeometry x="0.1" relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- OrgUnit → Staff -->
|
||||||
|
<mxCell id="e-org-staff" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="staff" parent="region-org">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-org-staff-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-staff"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- District → BusinessArea -->
|
||||||
|
<mxCell id="e-dist-biz" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="business-areas" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-biz-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-biz"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- District → Schools -->
|
||||||
|
<mxCell id="e-dist-school" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="schools" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-school-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-school"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- District → Complexes -->
|
||||||
|
<mxCell id="e-dist-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-dist-complex-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-complex"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- BusinessArea ↔ Complexes via join -->
|
||||||
|
<mxCell id="e-biz-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="business-areas" target="complex-biz-areas" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-join-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-biz-areas" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Schools ↔ Complexes via join -->
|
||||||
|
<mxCell id="e-school-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="schools" target="complex-schools" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-school-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-schools" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Complexes → complex_aliases -->
|
||||||
|
<mxCell id="e-complex-alias" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-aliases" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-alias-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-alias"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Complexes → complex_photos -->
|
||||||
|
<mxCell id="e-complex-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-photos" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Complexes → complex_price_trends -->
|
||||||
|
<mxCell id="e-complex-trend" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-price-trends" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-trend-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-trend"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Complexes → Buildings -->
|
||||||
|
<mxCell id="e-complex-bldg" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="buildings" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-bldg-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-bldg"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Buildings → RoomUnits -->
|
||||||
|
<mxCell id="e-bldg-room" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="room-units" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-room-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-bldg-room"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- MetroLine → MetroStation -->
|
||||||
|
<mxCell id="e-metro-line-station" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="metro-lines" target="metro-stations" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-metro-line-station"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- MetroStation ↔ Complexes via join -->
|
||||||
|
<mxCell id="e-metro-join1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="metro-stations" target="complex-metro-stations" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-metro-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-metro-stations" target="complexes" parent="region-complex">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → PropertyContacts -->
|
||||||
|
<mxCell id="e-prop-contact" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-contacts" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-contact-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-contact"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → FollowLogs -->
|
||||||
|
<mxCell id="e-prop-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-follow-logs" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → ListingHistories -->
|
||||||
|
<mxCell id="e-prop-listing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="listing-histories" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-listing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-listing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Photos -->
|
||||||
|
<mxCell id="e-prop-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-photos" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Keys -->
|
||||||
|
<mxCell id="e-prop-keys" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-keys" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-keys-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-keys"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Commissions -->
|
||||||
|
<mxCell id="e-prop-comm" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-commissions" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-comm-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-comm"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Inspections -->
|
||||||
|
<mxCell id="e-prop-insp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-inspections" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-insp-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-insp"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Marketing (1:1) -->
|
||||||
|
<mxCell id="e-prop-marketing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-marketing" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-marketing-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-marketing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Certificates (1:1) -->
|
||||||
|
<mxCell id="e-prop-cert" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-certificates" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-cert-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-cert"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Completeness (1:1) -->
|
||||||
|
<mxCell id="e-prop-score" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="completeness-scores" parent="region-property">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-score-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-score"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Clients → ClientRequirements -->
|
||||||
|
<mxCell id="e-client-req" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-requirements" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-req-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-req"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Clients → FollowLogs -->
|
||||||
|
<mxCell id="e-client-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-follow-logs" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Clients → Viewings -->
|
||||||
|
<mxCell id="e-client-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-viewings" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-viewing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Clients → Matches -->
|
||||||
|
<mxCell id="e-client-match" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-matches" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-match-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-match"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Clients → StatusLogs -->
|
||||||
|
<mxCell id="e-client-statuslog" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-status-logs" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-statuslog-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-statuslog"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Clients → FavFolders -->
|
||||||
|
<mxCell id="e-client-fav" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-fav-folders" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-client-fav-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-fav"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- FavFolders → FolderItems -->
|
||||||
|
<mxCell id="e-fav-items" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="client-fav-folders" target="client-folder-items" parent="region-client">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-fav-items-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-fav-items"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- CROSS-REGION EDGES (parent=1) -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Complexes → Properties -->
|
||||||
|
<mxCell id="e-complex-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=0;endArrow=ERmany;startArrow=ERone;fontSize=9;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" source="complexes" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-complex-prop-lbl" value="1:N complex_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-complex-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Buildings → Properties -->
|
||||||
|
<mxCell id="e-bldg-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-bldg-prop-lbl" value="1:N building_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-bldg-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- RoomUnits → Properties -->
|
||||||
|
<mxCell id="e-room-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="room-units" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-room-prop-lbl" value="1:N room_unit_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-room-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Staff → Properties (agent_id) -->
|
||||||
|
<mxCell id="e-staff-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="properties" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-prop-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Staff → Clients (agent_id) -->
|
||||||
|
<mxCell id="e-staff-client" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="clients" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-staff-client-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-client"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Viewings (cross-region) -->
|
||||||
|
<mxCell id="e-prop-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-viewings" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-viewing-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → Matches (cross-region) -->
|
||||||
|
<mxCell id="e-prop-match" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-matches" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-match-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-match"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
<!-- Properties → FolderItems (cross-region) -->
|
||||||
|
<mxCell id="e-prop-folder" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-folder-items" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e-prop-folder-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-folder"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
177
Project/fonrey/PRD/PERSONA_定义.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Fonrey 系统角色 Persona 定义
|
||||||
|
|
||||||
|
**版本**:v1.0
|
||||||
|
**状态**:已定稿
|
||||||
|
**作者**:产品团队
|
||||||
|
**最后更新**:2026-04-30
|
||||||
|
|
||||||
|
> 本文档是 Fonrey 所有 PRD、DATA_MODEL、PERMISSION_SEED 等文档的角色命名**唯一权威来源**。
|
||||||
|
> 所有其他文档中的角色称谓**必须**以本文档为准,禁止自造名称或混用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、角色层级概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Fonrey SaaS 平台 │
|
||||||
|
│ │
|
||||||
|
│ ① Platform Admin(平台超级管理员) │
|
||||||
|
│ └── 管理所有租户:开通、暂停、配置、版本升级 │
|
||||||
|
│ │
|
||||||
|
│ ─ ─ ─ ─ ─ ─ ─ 租 户 边 界 ─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||||
|
│ │
|
||||||
|
│ ② Tenant Admin(租户管理员) │
|
||||||
|
│ └── 管理本租户:组织架构、账号、权限、系统配置 │
|
||||||
|
│ │
|
||||||
|
│ ③ Agent(经纪人) │
|
||||||
|
│ └── 日常业务操作:录入房源、跟进客源、查看数据 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、Persona 详细定义
|
||||||
|
|
||||||
|
### P1 — Platform Admin(平台超级管理员)
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
| ---------------- | --------------------- |
|
||||||
|
| **Persona 名称** | Platform Admin |
|
||||||
|
| **中文称谓** | 平台超级管理员 |
|
||||||
|
| **Persona Code** | `PLATFORM_ADMIN` |
|
||||||
|
| **所属层** | 平台层(Platform) |
|
||||||
|
| **账号归属** | 平台运营团队(Fonrey 公司内部人员) |
|
||||||
|
| **账号数量** | 极少,手动创建,不通过租户系统管理 |
|
||||||
|
| **认证入口** | 独立管理后台(非租户客户端) |
|
||||||
|
| **Schema 归属** | `public`(跨所有租户) |
|
||||||
|
|
||||||
|
**职责范围:**
|
||||||
|
- 创建、暂停、注销租户(Tenant)
|
||||||
|
- 为租户初始化 Tenant Admin 账号
|
||||||
|
- 管理平台版本、发布客户端安装包
|
||||||
|
- 监控系统健康状态、查看平台级日志
|
||||||
|
- 配置平台级参数(短信网关、OSS、第三方集成等)
|
||||||
|
|
||||||
|
**不涉及:**
|
||||||
|
- 不进入任何租户的业务数据
|
||||||
|
- 不参与租户内部的权限分配、组织管理
|
||||||
|
|
||||||
|
**关键约束:**
|
||||||
|
- Platform Admin 走**独立认证体系**,不纳入租户 RBAC 权限模型
|
||||||
|
- 无需在 `permission_definitions` 中注册权限项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2 — Tenant Admin(租户管理员)
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
| ---------------- | ---------------------------------- |
|
||||||
|
| **Persona 名称** | Tenant Admin |
|
||||||
|
| **中文称谓** | 租户管理员 |
|
||||||
|
| **Persona Code** | `TENANT_ADMIN` |
|
||||||
|
| **所属层** | 租户层(Tenant) |
|
||||||
|
| **账号归属** | 各租户(房产经纪公司)的系统管理员 |
|
||||||
|
| **账号数量** | 每个租户 1~3 个,由 Platform Admin 创建初始账号 |
|
||||||
|
| **认证入口** | 与经纪人相同的租户客户端(Electron App) |
|
||||||
|
| **登录账号** | 平台运营分配的自定义字符串(不限于手机号格式) |
|
||||||
|
| **初始密码** | 平台统一固定初始密码,首次登录强制修改 |
|
||||||
|
| **Schema 归属** | 租户 Schema(`tenant_{id}`) |
|
||||||
|
| | |
|
||||||
|
|
||||||
|
**职责范围:**
|
||||||
|
- 维护组织架构(部门/门店树)
|
||||||
|
- 办理员工入职、离职、调动
|
||||||
|
- 创建和管理员工系统账号
|
||||||
|
- 配置角色与权限(角色创建、权限分配、个人权限调整)
|
||||||
|
- 配置系统枚举值(Lookup Items)、房源录入规则、客源规则
|
||||||
|
- 查看全租户范围的业务数据(受数据权限规则约束)
|
||||||
|
|
||||||
|
**不涉及:**
|
||||||
|
- 不能跨租户操作
|
||||||
|
- 不能修改平台级配置(版本、短信网关等)
|
||||||
|
|
||||||
|
**与「经纪人」的区别:**
|
||||||
|
- Tenant Admin 是**管理身份**,不从事日常房源/客源业务操作
|
||||||
|
- 在系统内显示为「管理员」身份,拥有全模块管理权限
|
||||||
|
- 可以同时持有某个业务角色(如总经),但账号性质以 Tenant Admin 为主
|
||||||
|
|
||||||
|
**在 PRD 中的出现场景:**
|
||||||
|
- 组织人事管理模块的主操作者
|
||||||
|
- 权限管理模块的主操作者
|
||||||
|
- 系统配置模块的主操作者
|
||||||
|
- 在楼盘管理、发布管理中执行管理操作的用户
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P3 — Agent(经纪人)
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
| ---------------- | ----------------------------- |
|
||||||
|
| **Persona 名称** | Agent |
|
||||||
|
| **中文称谓** | 经纪人 |
|
||||||
|
| **Persona Code** | `AGENT` |
|
||||||
|
| **所属层** | 租户层(Tenant) |
|
||||||
|
| **账号归属** | 各租户的在职员工 |
|
||||||
|
| **账号数量** | 每个租户 N 个,由 Tenant Admin 创建 |
|
||||||
|
| **认证入口** | 租户客户端(Electron App) |
|
||||||
|
| **登录账号** | 手机号(由 Tenant Admin 录入员工时自动创建) |
|
||||||
|
| **初始密码** | 系统统一固定初始密码,首次登录强制修改 |
|
||||||
|
| **Schema 归属** | 租户 Schema(`tenant_{id}`) |
|
||||||
|
|
||||||
|
**内部岗位子类型(Agent Sub-roles):**
|
||||||
|
|
||||||
|
Agent 是统称,内部通过「角色」区分岗位权限层级。系统内置以下角色(不可删除):
|
||||||
|
|
||||||
|
| 角色名称 | Role Code | 典型岗位 | 数据权限范围 |
|
||||||
|
| ---- | ---------------- | -------- | --------- |
|
||||||
|
| 置业顾问 | `ROLE_AGENT` | 一线经纪人 | 仅自己的数据 |
|
||||||
|
| 店管 | `ROLE_STORE_MGR` | 门店店长 | 本门店数据 |
|
||||||
|
| 区管 | `ROLE_AREA_MGR` | 区域经理 | 本区域数据 |
|
||||||
|
| 区总 | `ROLE_AREA_DIR` | 区域总监 | 本区域+下属区数据 |
|
||||||
|
| 副总 | `ROLE_VP` | 副总经理 | 全公司数据(部分) |
|
||||||
|
| 总经 | `ROLE_GM` | 总经理 | 全公司数据 |
|
||||||
|
| 其他职能 | `ROLE_OTHER` | 行政/财务/HR | 按需配置 |
|
||||||
|
|
||||||
|
**在 PRD 中的出现场景:**
|
||||||
|
- 房源管理模块的主操作者
|
||||||
|
- 客源管理模块的主操作者
|
||||||
|
- 楼盘管理模块的查看用户
|
||||||
|
- 登录模块的主要使用者
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、命名规范与替换对照表
|
||||||
|
|
||||||
|
以下为历史文档中出现的混乱称谓,与标准称谓的对照关系:
|
||||||
|
|
||||||
|
| 历史称谓(禁止继续使用) | 应替换为 | 说明 |
|
||||||
|
| ------------ | ----------------------- | ---------------- |
|
||||||
|
| 超级管理员 | Platform Admin(平台超级管理员) | 仅指平台层 |
|
||||||
|
| 平台管理员 | Platform Admin(平台超级管理员) | 同上 |
|
||||||
|
| 系统管理员 | Tenant Admin(租户管理员) | 租户层管理员 |
|
||||||
|
| 管理员(泛称) | Tenant Admin(租户管理员) | 明确指向租户层 |
|
||||||
|
| HR 管理员 | Tenant Admin(租户管理员) | 同一人,不区分子角色 |
|
||||||
|
| HR 行政 | Tenant Admin(租户管理员) | 同上 |
|
||||||
|
| 一线经纪人 | Agent(经纪人) | 统称,角色层面才区分岗位 |
|
||||||
|
| 置业顾问(作为用户称谓) | Agent(经纪人) | 置业顾问仅作为「内置角色名」使用 |
|
||||||
|
|
||||||
|
**规则说明:**
|
||||||
|
1. PRD User Story 的 **As a** 部分:使用中文称谓(`Tenant Admin(租户管理员)`、`Agent(经纪人)`)
|
||||||
|
2. 权限矩阵、DATA_MODEL 注释:使用 Persona Code(`TENANT_ADMIN`、`AGENT`)
|
||||||
|
3. 角色名称(置业顾问、店管、总经等)**仅在角色管理相关语境中**出现,不作为用户身份称谓
|
||||||
|
4. `Platform Admin` 在各 PRD 中尽量少出现,其功能属于「系统管理模块」,不在各子模块 PRD 内展开
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、快速索引
|
||||||
|
|
||||||
|
| 场景 | 使用 |
|
||||||
|
|------|------|
|
||||||
|
| PRD User Story 主语 | `Tenant Admin(租户管理员)` / `Agent(经纪人)` |
|
||||||
|
| 错误提示文案(面向用户) | 「请联系您的租户管理员」 |
|
||||||
|
| DATA_MODEL 注释 | `created_by: 创建该记录的 Agent(经纪人)用户 ID` |
|
||||||
|
| 权限矩阵行标题 | 使用角色名(置业顾问 / 店管 / 总经 ...) |
|
||||||
|
| 代码注释 / 枚举值 | `PLATFORM_ADMIN` / `TENANT_ADMIN` / `AGENT` |
|
||||||
|
| 日志、审计记录 | `operator_type: PLATFORM_ADMIN / TENANT_ADMIN / AGENT` |
|
||||||
287
Project/fonrey/PRD/PRD_MVP.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
# Fonrey 房睿 — MVP 范围书
|
||||||
|
|
||||||
|
**Status**: Draft
|
||||||
|
**Author**: Product Team
|
||||||
|
**Last Updated**: 2026-04-24
|
||||||
|
**Version**: 1.0
|
||||||
|
|
||||||
|
> **For AI assistants**: 本文件定义 Phase 1(MVP)的边界。在任何功能实现前,先对照本文确认是否在范围内。范围外的功能禁止在 MVP 阶段实现。
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 作者 | 变更说明 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| v1.0 | 2026-04-24 | Product Team | 初稿:定义 Phase 1(MVP)边界与范围 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 产品背景与目标
|
||||||
|
|
||||||
|
**Fonrey(房睿)** 是一套面向中小型房产经纪公司的 B2B SaaS 管理平台,解决以下核心痛点:
|
||||||
|
|
||||||
|
- 房源/客源信息散乱,全靠人工记录
|
||||||
|
- 跟进记录缺失,数据流失严重
|
||||||
|
- 重复录入浪费大量经纪人时间
|
||||||
|
- 无法支撑 89,000+ 数据量级下的高效房客匹配
|
||||||
|
|
||||||
|
**MVP 目标**:在一家种子客户(单租户)环境下,完整跑通"录入房源 → 录入客源 → 匹配带看 → 成交"的核心业务链路。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. MVP 核心功能清单(Phase 1 必须实现)
|
||||||
|
|
||||||
|
### 2.1 优先级定义
|
||||||
|
|
||||||
|
| 优先级 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| **P0** | MVP 上线前必须完成,阻断核心业务链路 |
|
||||||
|
| **P1** | MVP 上线后第一个迭代周期内完成 |
|
||||||
|
| **P2** | 已规划,列入路线图但不阻断上线 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 模块优先级矩阵
|
||||||
|
|
||||||
|
#### 🏠 房源管理
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 录入住宅(二手出售/出租) | **P0** | 核心业务入口 |
|
||||||
|
| 房源列表(二手&租赁) | **P0** | 含筛选、排序、分页 |
|
||||||
|
| 房源详情页 | **P0** | 含基本信息、产证、交易信息展示 |
|
||||||
|
| 跟进记录(全部/写入/修改/其他) | **P0** | 含钥匙、委托、实勘 |
|
||||||
|
| 图片管理(相册上传/分类/排序) | **P0** | 核心房源内容 |
|
||||||
|
| 业主联系人管理 | **P0** | 含新增/编辑/查看同业主房源 |
|
||||||
|
| 价格调整(调价/调价记录) | **P0** | 核心运营操作 |
|
||||||
|
| 房源状态变更(在售/暂缓/成交/下架) | **P0** | 状态机核心 |
|
||||||
|
| 房源维护完成度(诊断面板) | **P1** | 提升数据质量 |
|
||||||
|
| 敏感信息跟进(查看权限控制) | **P1** | 需配合权限模块 |
|
||||||
|
| 附件管理 | **P1** | 非阻断性 |
|
||||||
|
| 市场报盘 | **P1** | 运营辅助功能 |
|
||||||
|
| 价格解读 | **P1** | 分析辅助 |
|
||||||
|
| 录入别墅/商铺/商住/写字楼/其他 | **P2** | 住宅优先,商业类低频 |
|
||||||
|
| 全部商铺列表 / 全部写字楼列表 | **P2** | 配合 P2 录入功能 |
|
||||||
|
| 房源广场 | **P2** | 跨租户/公共池功能 |
|
||||||
|
|
||||||
|
#### 🏙️ 楼盘管理
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 楼盘列表 + 楼盘详情(楼盘信息/楼栋/结构) | **P0** | 房源数据底座,必须先行 |
|
||||||
|
| 区域管理(城区/商圈) | **P0** | 房源关联必须 |
|
||||||
|
| 楼盘照片管理 | **P1** | 数据完善 |
|
||||||
|
| 楼盘价格走势 | **P1** | 分析辅助 |
|
||||||
|
| 周边配套(学校管理) | **P1** | 补充信息 |
|
||||||
|
| 应用数据标准 | **P2** | 明确不做 |
|
||||||
|
|
||||||
|
#### 👥 客源管理
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 录入私客(求购/求租) | **P0** | 核心业务 |
|
||||||
|
| 私客列表(全部/求购/求租) | **P0** | 含筛选、排序 |
|
||||||
|
| 私客详情(基本信息/需求信息) | **P0** | |
|
||||||
|
| 跟进记录(全部/写入/修改/其他) | **P0** | |
|
||||||
|
| 带看管理(预约带看/新增带看) | **P0** | 房客匹配核心 |
|
||||||
|
| 联系人管理 | **P0** | |
|
||||||
|
| 客源状态变更(改等级/改状态) | **P0** | |
|
||||||
|
| 转公客 / 转成交 / 转无效 | **P0** | 生命周期核心 |
|
||||||
|
| 二手配房(智能匹配) | **P1** | 核心价值,但可后续迭代 |
|
||||||
|
| 客源解读 | **P1** | AI 辅助分析 |
|
||||||
|
| 客源信息概览 | **P1** | 汇总视图 |
|
||||||
|
| 客源收藏夹 | **P1** | 辅助功能 |
|
||||||
|
| 公客管理 | **P2** | 私客优先 |
|
||||||
|
| 成交客管理 | **P2** | |
|
||||||
|
| 暂缓私客 | **P2** | |
|
||||||
|
|
||||||
|
#### 🏢 组织人事
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 公司组织结构(部门/门店树) | **P0** | 权限系统基础 |
|
||||||
|
| 员工列表/员工详情 | **P0** | |
|
||||||
|
| 员工入职/账号创建 | **P0** | |
|
||||||
|
| 员工离职 / 调动 | **P1** | |
|
||||||
|
| 员工通讯录 | **P1** | |
|
||||||
|
| 异动记录 | **P1** | |
|
||||||
|
| 奖惩记录 | **P2** | |
|
||||||
|
| 职务管理 | **P1** | |
|
||||||
|
| 门店分布地图 | **P2** | |
|
||||||
|
|
||||||
|
#### 🔐 权限管理
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 角色管理(预设角色 + 自定义角色) | **P0** | 权限基础 |
|
||||||
|
| 人员权限列表 | **P0** | |
|
||||||
|
| 角色批量分配 | **P0** | |
|
||||||
|
| 功能权限(菜单级) | **P0** | |
|
||||||
|
| 数据权限(部门/个人/全司) | **P0** | |
|
||||||
|
| 字段级权限(敏感字段可见性) | **P1** | 配合房源/客源敏感信息 |
|
||||||
|
| 个人特定权限覆盖 | **P1** | |
|
||||||
|
|
||||||
|
#### 🔑 用户登录
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 账号密码登录 | **P0** | |
|
||||||
|
| 多租户识别(子域名/域名) | **P0** | |
|
||||||
|
| Token 管理 / 会话超时 | **P0** | |
|
||||||
|
| 短信验证码登录 | **P1** | |
|
||||||
|
| 密码重置 | **P1** | |
|
||||||
|
| 记住登录状态 | **P1** | |
|
||||||
|
|
||||||
|
#### ⚙️ 系统配置
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 首页设置 | **P1** | |
|
||||||
|
| 房源设置(字段必填/自定义字段/标签) | **P0** | 影响录入表单 |
|
||||||
|
| 相关方设置 | **P1** | |
|
||||||
|
| 客源设置(基本配置/参数配置) | **P1** | |
|
||||||
|
| 人事OA设置 | **P2** | 依赖人事OA模块完整规划,本期不实现 |
|
||||||
|
| 交易设置 | **P2** | 依赖交易模块完整规划,本期不实现 |
|
||||||
|
| 财务设置 | **Out of Scope** | 消费方「财务管理模块」为 Out of Scope,配置面板无意义,随模块一并排除 |
|
||||||
|
| 合同设置 | **Out of Scope** | 消费方「合同管理模块」为 Out of Scope,配置面板无意义,随模块一并排除 |
|
||||||
|
|
||||||
|
#### 🖥️ 系统管理(运营后台)
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 租户管理(开通/暂停/配置) | **P1** | 单租户种子阶段可手动 |
|
||||||
|
| 系统健康监控 | **P1** | |
|
||||||
|
| 操作审计日志 | **P2** | |
|
||||||
|
| 灰度发布 / 滚动升级 | **P2** | |
|
||||||
|
|
||||||
|
#### 💻 客户端发布
|
||||||
|
|
||||||
|
| 功能 | 优先级 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| Windows 桌面客户端(内置浏览器) | **P1** | 种子客户使用 Web 端可先行 |
|
||||||
|
| 自动更新机制 | **P1** | 配合客户端 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 非目标(Out of Scope — MVP 阶段绝对不做)
|
||||||
|
|
||||||
|
以下功能在 MVP 阶段**明确不实现**,AI 生成代码时不得为这些功能预留接口或引入相关依赖:
|
||||||
|
|
||||||
|
| 功能 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| 移动端适配 | v2 规划 |
|
||||||
|
| 新房模块(新房管理/新房设置) | 独立模块,后续版本 |
|
||||||
|
| 合同管理模块 | 独立模块,后续版本 |
|
||||||
|
| 财务管理/提成结算 | 独立模块,后续版本 |
|
||||||
|
| 三网发布(安居客/链家/贝壳对接) | 独立模块,后续版本 |
|
||||||
|
| 数据报表/行程量化 | 独立模块,后续版本 |
|
||||||
|
| 在线充值/增值服务 | 独立模块,后续版本 |
|
||||||
|
| 任务管理(OA任务/入职祝福) | 低优先 |
|
||||||
|
| 考勤管理 | 独立 HR 模块 |
|
||||||
|
| 审批流程 | 独立 OA 模块 |
|
||||||
|
| 智慧大屏 / VR换装 | 增值产品 |
|
||||||
|
| 房源广场(跨租户公共池) | 多租户复杂场景 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 用户故事(MVP 核心路径)
|
||||||
|
|
||||||
|
### Story 1 — 经纪人录入房源
|
||||||
|
> As a **Agent(经纪人)**,
|
||||||
|
> I want to **快速录入一套二手住宅并上传图片和业主联系方式**,
|
||||||
|
> So that **这套房源的信息能被团队所有成员找到和跟进**.
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 可在 3 分钟内完成住宅基本信息录入
|
||||||
|
- 上传图片后自动按分类展示
|
||||||
|
- 录入后即刻出现在房源列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 2 — 经纪人跟进房源
|
||||||
|
> As a **Agent(经纪人)**,
|
||||||
|
> I want to **对我负责的房源记录每次跟进(面访/电话/钥匙/实勘)**,
|
||||||
|
> So that **我的跟进历史有据可查,团队不会重复联系同一业主**.
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 跟进记录按时间线倒序展示
|
||||||
|
- 支持写入跟进、修改跟进、其他跟进(钥匙/委托/实勘)
|
||||||
|
- 敏感信息跟进只对有权限的人员可见
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 3 — 经纪人录入客源
|
||||||
|
> As a **Agent(经纪人)**,
|
||||||
|
> I want to **录入意向购房/租房客户并跟进其需求变化**,
|
||||||
|
> So that **我能在合适时机将客户与合适房源匹配**.
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 区分求购/求租两种意向
|
||||||
|
- 支持跟进记录
|
||||||
|
- 可安排带看并记录带看结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 4 — 转成交
|
||||||
|
> As a **Agent(经纪人)**,
|
||||||
|
> I want to **将已达成交易的客源标记为"成交"并关联成交房源**,
|
||||||
|
> So that **成交数据进入系统留存,房源状态自动更新**.
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 转成交时必须选择关联房源
|
||||||
|
- 成交后客源状态自动变为"成交客"
|
||||||
|
- 关联房源状态建议变更为"成交"(可手动确认)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 5 — 店长查看团队数据
|
||||||
|
> As a **门店店长**,
|
||||||
|
> I want to **查看本门店所有员工的房源和客源列表**,
|
||||||
|
> So that **我能掌握团队整体情况并合理分配资源**.
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 数据权限按部门隔离,店长可见本门店数据
|
||||||
|
- 可筛选查看特定员工的房源/客源
|
||||||
|
- 无法看到其他门店的数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. MVP 技术边界
|
||||||
|
|
||||||
|
| 约束 | 决策 |
|
||||||
|
|------|------|
|
||||||
|
| 租户数 | **单租户**种子阶段,多租户架构已就位但不激活多租户切换 UI |
|
||||||
|
| 数据量 | 目标支撑 **89,000 条**房源,测试阶段以 10,000 条压测 |
|
||||||
|
| 浏览器支持 | Chrome 最新版 / Edge 最新版,不支持 IE |
|
||||||
|
| 语言 | 简体中文,不做国际化 |
|
||||||
|
| 移动端 | **不做**,Web 端 Desktop-first |
|
||||||
|
| 导出 | Excel/CSV 导出通过 Celery 异步,不超时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. MVP 交付检查清单
|
||||||
|
|
||||||
|
在 MVP 正式上线前,以下项目必须全部勾选:
|
||||||
|
|
||||||
|
- [ ] 房源录入(住宅)完整流程可用
|
||||||
|
- [ ] 房源列表可筛选/排序/分页
|
||||||
|
- [ ] 客源录入(求购/求租)完整流程可用
|
||||||
|
- [ ] 带看创建与记录可用
|
||||||
|
- [ ] 转成交流程可用
|
||||||
|
- [ ] 楼盘数据可录入(为房源提供底座)
|
||||||
|
- [ ] 员工账号可创建/分配角色
|
||||||
|
- [ ] 权限隔离:经纪人只能看自己数据,店长能看本店数据
|
||||||
|
- [ ] 89,000 条数据量下列表查询 < 2 秒(含索引优化)
|
||||||
|
- [ ] 图片上传到 Cloudflare R2 可用
|
||||||
|
- [ ] 多租户 Schema 隔离验证通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 版本路线图
|
||||||
|
|
||||||
|
| 版本 | 目标 | 核心功能 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **v0.1 MVP** | 单租户种子验证 | P0 功能全部上线 |
|
||||||
|
| **v0.2** | 功能完善 | P1 功能上线,开始多租户测试 |
|
||||||
|
| **v0.3** | 商业化就绪 | Windows 客户端、多租户正式开放、系统配置完善 |
|
||||||
|
| **v1.0** | 正式发布 | 新房模块、合同/财务模块路线图确认 |
|
||||||
860
Project/fonrey/PRD/TASK.md
Normal file
@@ -0,0 +1,860 @@
|
|||||||
|
## 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-A](#US-SETTING-001-A-管理员配置可选枚举值-Lookup-Items) | 系统配置 | 管理员配置可选枚举值(Lookup Items) | [ ] |
|
||||||
|
| [US-SETTING-001-B](#US-SETTING-001-B-管理员配置房源字段必填规则) | 系统配置 | 管理员配置房源字段必填规则 | [ ] |
|
||||||
|
| [US-SETTING-001-C](#US-SETTING-001-C-管理员配置客源录入规则) | 系统配置 | 管理员配置客源录入规则(查重范围/必填字段) | [ ] |
|
||||||
|
|
||||||
|
#### 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-Platform Admin(平台超级管理员)管理租户开通暂停配置) | 系统管理 | Platform Admin(平台超级管理员)管理租户(开通/暂停/配置) | [ ] |
|
||||||
|
| [US-SYSTEM-011](#US-SYSTEM-011-Platform Admin(平台超级管理员)监控系统健康状态) | 系统管理 | Platform Admin(平台超级管理员)监控系统健康状态 | [ ] |
|
||||||
|
| [US-RELEASE-010](#US-RELEASE-010-系统发布Windows桌面客户端安装包) | 客户端发布 | 系统发布Windows桌面客户端安装包 | [ ] |
|
||||||
|
| [US-RELEASE-011](#US-RELEASE-011-客户端自动检测并更新至最新版本) | 客户端发布 | 客户端自动检测并更新至最新版本 | [ ] |
|
||||||
|
|
||||||
|
#### 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~~ | ~~系统配置~~ | ~~管理员配置财务规则~~ — **已移出,财务模块 Out of Scope** | ~~[ ]~~ |
|
||||||
|
| ~~US-SETTING-023~~ | ~~系统配置~~ | ~~管理员配置合同模板~~ — **已移出,合同模块 Out of Scope** | ~~[ ]~~ |
|
||||||
|
| [US-SYSTEM-020](#US-SYSTEM-020-Platform Admin(平台超级管理员)查看操作审计日志) | 系统管理 | Platform Admin(平台超级管理员)查看操作审计日志 | [ ] |
|
||||||
|
| [US-SYSTEM-021](#US-SYSTEM-021-Platform Admin(平台超级管理员)管理灰度发布滚动升级) | 系统管理 | Platform Admin(平台超级管理员)管理灰度发布/滚动升级 | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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-A 管理员配置可选枚举值 Lookup Items
|
||||||
|
|
||||||
|
- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置模块PRD.md` - US-SETTING-001-A
|
||||||
|
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL.md`(待 Atlas 补充 `lookup_items` DDL)
|
||||||
|
- 状态:[ ]
|
||||||
|
- 验收标准:
|
||||||
|
- 管理员进入「系统设置 → 参数配置」,页面按模块分组展示所有可配置参数项(客源来源、跟进目的、房源来源)
|
||||||
|
- 可新增自定义选项,新选项追加至列表末尾并立即对经纪人录入下拉生效(刷新后)
|
||||||
|
- 系统预制选项(`is_system=True`)不可删除,仅可停用;停用后前端下拉不再展示,历史数据保留并标注「已停用」
|
||||||
|
- 支持调整选项排序(拖拽或修改排序值),排序变更在保存后前端生效
|
||||||
|
- 配置保存时主动失效 Redis 缓存 key `{tenant_schema}:setting:lookup:{module}.{key}`,最长 5 分钟延迟
|
||||||
|
|
||||||
|
##### US-SETTING-001-B 管理员配置房源字段必填规则
|
||||||
|
|
||||||
|
- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置模块PRD.md` - US-SETTING-001-B
|
||||||
|
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL.md`(待 Atlas 补充 `field_requirement_rules` DDL)、`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||||
|
- 状态:[ ]
|
||||||
|
- 验收标准:
|
||||||
|
- 管理员进入「系统设置 → 房源字段规则」,以「用途 × 交易状态」矩阵展示配置(住宅×出售、住宅×出租)
|
||||||
|
- 每个字段显示当前规则(必填 / 选填 / 隐藏),以三态 Toggle 或 Radio 形式编辑,保存后生效
|
||||||
|
- 规则应用于录入界面:必填字段显示「*」标记,提交时为空则拦截;隐藏字段不渲染
|
||||||
|
- 规则变更仅影响新录入,不影响存量房源数据
|
||||||
|
- MVP 可配置字段:朝向、装修情况、楼层、建筑面积、套内面积、房型、产权年限、车位数
|
||||||
|
|
||||||
|
##### US-SETTING-001-C 管理员配置客源录入规则
|
||||||
|
|
||||||
|
- 参考PRD文档:`Project/fonrey/PRD/系统配置/系统配置模块PRD.md` - US-SETTING-001-C
|
||||||
|
- 参考DATA_MODEL文档:`Project/fonrey/DATA_MODEL/DATA_MODEL.md`(待 Atlas 补充 `tenant_settings` DDL)、`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
|
||||||
|
- 状态:[ ]
|
||||||
|
- 验收标准:
|
||||||
|
- 管理员进入「系统设置 → 客源规则」,可配置新增私客时的查重范围:本人(默认)/ 本部门 / 全公司
|
||||||
|
- 查重规则实时生效:经纪人录入手机号失焦后,系统按当前查重范围加密比对,若重复则提示归属人和录入时间,经纪人可选「仍然录入」或「放弃」
|
||||||
|
- 可配置客源必填字段开关(等级、来源默认必填;总价区间、居室需求、购房目的默认选填)
|
||||||
|
- 配置保存时主动失效 Redis 缓存 key `{tenant_schema}:setting:client_rules`,经纪人下次打开录入界面即应用最新规则
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 Platform Admin(平台超级管理员)管理租户开通暂停配置
|
||||||
|
|
||||||
|
- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 租户管理(开通/暂停/配置)
|
||||||
|
- 状态:[ ]
|
||||||
|
- 验收标准:可在运营后台新开通租户(自动创建独立PostgreSQL Schema);可暂停租户(暂停后租户用户无法登录);可为租户配置域名/子域名;租户操作记录写入平台操作日志
|
||||||
|
|
||||||
|
##### US-SYSTEM-011 Platform Admin(平台超级管理员)监控系统健康状态
|
||||||
|
|
||||||
|
- 参考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 管理员配置财务规则~~
|
||||||
|
|
||||||
|
> ⛔ **已移出路线图**:消费方「财务管理/提成结算模块」在 MVP 及近期版本中明确为 Out of Scope(见 PRD_MVP.md §3),对应的系统配置面板无实际消费方,随模块一并排除。若后续财务模块立项,本 US 随之恢复。
|
||||||
|
|
||||||
|
##### ~~US-SETTING-023 管理员配置合同模板~~
|
||||||
|
|
||||||
|
> ⛔ **已移出路线图**:消费方「合同管理模块」在 MVP 及近期版本中明确为 Out of Scope(见 PRD_MVP.md §3),对应的系统配置面板无实际消费方,随模块一并排除。若后续合同模块立项,本 US 随之恢复。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 系统管理(运营后台)
|
||||||
|
|
||||||
|
##### US-SYSTEM-020 Platform Admin(平台超级管理员)查看操作审计日志
|
||||||
|
|
||||||
|
- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 操作审计日志
|
||||||
|
- 状态:[ ]
|
||||||
|
- 验收标准:(规划中,详细验收标准待PRD细化后补充)
|
||||||
|
|
||||||
|
##### US-SYSTEM-021 Platform Admin(平台超级管理员)管理灰度发布滚动升级
|
||||||
|
|
||||||
|
- 参考PRD文档:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 灰度发布/滚动升级
|
||||||
|
- 状态:[ ]
|
||||||
|
- 验收标准:(规划中,详细验收标准待PRD细化后补充)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4 - 明确不做(Out of Scope)
|
||||||
|
|
||||||
|
> 以下功能在 MVP 阶段及近期版本路线图中明确不实现,仅作备忘。
|
||||||
|
|
||||||
|
<!-- OUT OF SCOPE: 移动端适配 - v2 规划 -->
|
||||||
|
<!-- OUT OF SCOPE: 新房模块(新房管理/新房设置)- 独立模块,后续版本 -->
|
||||||
|
<!-- OUT OF SCOPE: 合同管理模块 - 独立模块,后续版本 -->
|
||||||
|
<!-- OUT OF SCOPE: 财务管理/提成结算 - 独立模块,后续版本 -->
|
||||||
|
<!-- OUT OF SCOPE: 三网发布(安居客/链家/贝壳对接)- 独立模块,后续版本 -->
|
||||||
|
<!-- OUT OF SCOPE: 数据报表/行程量化 - 独立模块,后续版本 -->
|
||||||
|
<!-- OUT OF SCOPE: 在线充值/增值服务 - 独立模块,后续版本 -->
|
||||||
|
<!-- OUT OF SCOPE: 任务管理(OA任务/入职祝福)- 低优先 -->
|
||||||
|
<!-- OUT OF SCOPE: 考勤管理 - 独立 HR 模块 -->
|
||||||
|
<!-- OUT OF SCOPE: 审批流程 - 独立 OA 模块 -->
|
||||||
|
<!-- OUT OF SCOPE: 智慧大屏 / VR换装 - 增值产品 -->
|
||||||
|
<!-- OUT OF SCOPE: 房源广场(跨租户公共池)- 多租户复杂场景 -->
|
||||||
|
<!-- OUT OF SCOPE: 资料客详细功能 - 后续版本规划 -->
|
||||||
|
<!-- OUT OF SCOPE: 营销客详细功能(含端口发布/朋友圈分享)- 营销模块另行规划 -->
|
||||||
|
<!-- OUT OF SCOPE: AI 智能推荐引擎自研 - 基础版基于规则匹配实现 -->
|
||||||
|
<!-- OUT OF SCOPE: 客源数据对外 API 开放 - 接口规范另行设计 -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 已完成
|
||||||
|
|
||||||
|
(暂无)
|
||||||
6818
Project/fonrey/PRD/TASK_AGENT_READY.md
Normal file
3612
Project/fonrey/PRD/TASK_AGENT_READY_P0.md
Normal file
2186
Project/fonrey/PRD/客源管理/客源管理模块PRD.md
Normal file
902
Project/fonrey/PRD/平台管理后台/平台管理后台PRD.md
Normal file
@@ -0,0 +1,902 @@
|
|||||||
|
# PRD:平台管理后台(Platform Admin Console)
|
||||||
|
|
||||||
|
**状态**:Draft
|
||||||
|
**作者**:产品经理
|
||||||
|
**最后更新**:2026-05-02
|
||||||
|
**版本**:v1.1
|
||||||
|
**关联模块**:权限管理(仅平台侧管理员账号体系)、登录管理(管理员登录与 MFA)、所有租户业务模块(间接,仅作运营/监控对象)
|
||||||
|
**利益相关方**:工程负责人、运营团队、安全合规、客户成功团队、IT 运维
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 作者 | 变更说明 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| v1.0 | 2026-05-02 | 产品经理 | 由 `PRD/系统管理/系统管理模块PRD.md` (v1.3) 与 `PRD/发布管理/客户端发布管理模块PRD.md` (v1.2) 合并而来;统一面向平台管理员视角,统一规划页面路由与业务 API 操作清单。原两份 PRD 文件同步删除,参见 `ADR-20260502-001`。 |
|
||||||
|
| v1.1 | 2026-05-02 | 产品经理 | **修复 P-B-1**:全部用户故事补齐功能编号 PA-001 ~ PA-011,与登录管理 PRD v3.0「功能 ID + 验收标准」风格对齐,建立需求→测试用例→Bug 单可追溯链条。**修复 X-1**:新增 PA-011「租户注销 SOP」用户故事(Persona E),覆盖删除前数据导出确认、双人复核审批流程、30 天冷静期可撤销机制;同步更新租户状态机(新增 Pending Cancellation 状态节点)、权限矩阵(新增注销审批相关操作行)、§5.1.4 删除章节(引用 PA-011 SOP)、§5.4 页面导航逻辑(注销审批待办流程)、租户列表筛选(新增 Pending Cancellation / Pending Delete 状态)、§5.5.1 API 操作清单(新增注销发起、审批、撤销、硬删除操作项)。 |
|
||||||
|
|
||||||
|
> **本 PRD 取代以下两份历史文档**:
|
||||||
|
> - `PRD/系统管理/系统管理模块PRD.md`(v1.3,已删除,被本 PRD 替代)
|
||||||
|
> - `PRD/发布管理/客户端发布管理模块PRD.md`(v1.2,已删除,被本 PRD 替代)
|
||||||
|
>
|
||||||
|
> 决策依据:`ADR-20260502-001`(REQ)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题陈述
|
||||||
|
|
||||||
|
### 1.1 背景
|
||||||
|
|
||||||
|
Fonrey(房睿)是面向房产经纪公司的 B2B SaaS 平台,采用 `django-tenants` 实现 PostgreSQL Schema 级别多租户隔离,同时通过 Electron 桌面客户端为终端经纪人提供统一入口。随着平台商业化推进,**平台运营团队**需要一套独立、跨租户的「平台管理后台」(Platform Admin Console)来统一承担:
|
||||||
|
|
||||||
|
1. 租户全生命周期管理(开通、挂起、注销、License 续期)
|
||||||
|
2. 平台版本治理(基础数据版本、租户数据版本、灰度升级、回滚)
|
||||||
|
3. 数据备份与恢复(灾难场景应对)
|
||||||
|
4. 客户端发布治理(桌面客户端版本上线、强制升级、版本分布监控)
|
||||||
|
5. 操作审计与合规
|
||||||
|
6. 平台管理员账号与安全(MFA、IP 白名单、强制登出)
|
||||||
|
|
||||||
|
平台管理后台部署于平台公共域名(与租户应用域名隔离),数据落在 `public` schema,跨租户。Tenant Admin 与 Agent 一律无访问权限。
|
||||||
|
|
||||||
|
### 1.2 核心痛点
|
||||||
|
|
||||||
|
| 痛点 | 影响方 | 当前代价 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| 无统一租户管理界面,开通 / 挂起 / 注销依赖人工脚本 | 运营团队 | 高错误风险,操作耗时 |
|
||||||
|
| 平台版本升级需停机,影响所有租户 | 全量用户 | SLA 违约风险 |
|
||||||
|
| 数据备份无策略,灾难恢复依赖人工 | 平台稳定性 | 数据丢失风险 |
|
||||||
|
| 高危操作无审计轨迹 | 管理层 / 合规 | 法律与客户信任风险 |
|
||||||
|
| 客户端浏览器版本碎片化,兼容性问题层出 | Agent / 客户成功 | 支持成本飙升 |
|
||||||
|
| 客户端无统一发布渠道,旧版本与新后端 API 不兼容 | 全量用户 | 数据错误风险 |
|
||||||
|
| 无法跨租户掌握客户端版本分布与覆盖率 | Platform Admin | 无法定向催更 |
|
||||||
|
|
||||||
|
### 1.3 目标用户
|
||||||
|
|
||||||
|
| 角色 | 使用场景 | 频率 |
|
||||||
|
|------|---------|------|
|
||||||
|
| Platform Admin(平台超级管理员) | 全局配置、高危操作、版本上线、强制升级 | 低频(每周) |
|
||||||
|
| 运营人员(Ops Operator) | 日常租户管理、监控巡检、客户咨询响应 | 高频(每日) |
|
||||||
|
| 只读审计员(Read-only Auditor) | 日志查询、合规报告导出 | 中频(每周) |
|
||||||
|
|
||||||
|
> 本 PRD **不涉及**租户内部的 Tenant Admin、Agent 角色——他们对本后台无访问权限。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目标与成功指标
|
||||||
|
|
||||||
|
| 目标 | 指标 | 当前基线 | 目标值 | 测量窗口 |
|
||||||
|
|------|------|---------|--------|---------|
|
||||||
|
| 租户开通效率 | 新租户开通耗时 | 人工脚本 ~30 分钟 | < 5 分钟(含自动初始化) | 上线后 30 天 |
|
||||||
|
| 平台升级零停机 | 升级期间受影响租户数 | 全量中断 | 灰度阶段 ≤ 5% 租户 | 每次升级 |
|
||||||
|
| 数据恢复能力 | RTO(单租户恢复时间) | 无标准流程 | < 2 小时 | v1 上线即达标 |
|
||||||
|
| 操作合规覆盖 | 高危操作审计日志覆盖率 | 0% | 100% | 上线后 30 天 |
|
||||||
|
| 管理员安全 | 平台管理员 MFA 启用率 | 0% | 100%(强制) | 上线即达标 |
|
||||||
|
| 客户端兼容性问题消除 | 因浏览器兼容产生的支持工单数 | 待统计 | 降低 ≥ 90% | 客户端上线后 60 天 |
|
||||||
|
| 客户端版本一致性 | 在线用户使用最新客户端比例 | 0%(无客户端) | ≥ 95% | 版本发布后 7 天 |
|
||||||
|
| 客户端自动更新成功率 | 收到通知 → 升级完成的成功率 | 无基准 | ≥ 98% | 每次发布后 48 小时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 非目标(Non-Goals)
|
||||||
|
|
||||||
|
**不在 v1 实现**:
|
||||||
|
- 自动化账单计费、多币种支持、Webhook 自定义集成市场
|
||||||
|
- 租户端自助迁移工具
|
||||||
|
- 客户响应数据导出请求(Story「响应客户数据导出请求」暂缓,列入后续迭代)
|
||||||
|
|
||||||
|
**不属于本后台**:
|
||||||
|
- 租户内业务权限的细粒度配置(见权限管理模块 PRD)
|
||||||
|
- 客服工单系统、SLA 自动赔付
|
||||||
|
- macOS / Linux 桌面客户端、移动端 App
|
||||||
|
- 客户端离线模式、私有化离线安装方案
|
||||||
|
- 客户端代码混淆 / 反逆向加固
|
||||||
|
|
||||||
|
**不支持**:
|
||||||
|
- 移动端浏览器访问平台管理后台(运营场景明确为 PC 桌面)
|
||||||
|
- 多语言界面(运营团队为内部人员,中文已满足)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 用户角色与核心故事
|
||||||
|
|
||||||
|
### Persona A — 运营人员 Lily(日常租户管理)
|
||||||
|
|
||||||
|
> 负责 Fonrey 的日常运营,每天处理新客户开通、异常租户处理、客户咨询。使用 PC 浏览器登录平台管理后台。
|
||||||
|
|
||||||
|
#### PA-001 Story A1:新租户开通
|
||||||
|
|
||||||
|
> 作为运营人员,我希望通过填写表单快速完成租户开通,并由系统自动完成数据库初始化与欢迎通知,无需手动执行脚本。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 表单提交后,系统在后台自动创建 PostgreSQL Schema 并注入默认配置(见 §5.1.2「默认配置内容」),完成耗时 < 60 秒
|
||||||
|
- [ ] 默认配置注入包含两部分:
|
||||||
|
- **权限定义(PermissionDef)**:注入平台所有权限码定义(`permission_code`、描述、模块归属),作为该租户 RBAC 体系基础
|
||||||
|
- **系统默认角色与权限绑定**:按「角色权限矩阵.md」注入 7 个系统内置业务角色(置业顾问、店管、区管、区总、副总、总经、其他职能),并完成角色—权限绑定
|
||||||
|
- [ ] 初始 Tenant Admin(以联系人手机号创建)**不通过业务角色赋权**,由系统在租户创建时直接写入「租户管理员」专属权限集合;该集合独立于 7 个业务角色之外,不在租户角色管理界面显示,不可由 Tenant Admin 自行修改或分配。新增/变更租户管理员须由平台运营方在本后台操作
|
||||||
|
- [ ] 新租户创建后:平台运营管理员收到站内消息;租户联系人收到欢迎邮件(见下方「欢迎邮件规范」)
|
||||||
|
- [ ] 联系人无邮箱时,运营人员可在租户详情页下载「入驻信息 PDF 文档」,通过微信等渠道转发
|
||||||
|
- [ ] 租户访问地址采用统一域名 + Tenant Code 参数形式(`https://app.fonrey.com/?tenant={Tenant Code}`),无需子域名;详情页展示该链接,可一键复制
|
||||||
|
- [ ] 创建失败时回滚所有已创建资源,并显示明确错误原因
|
||||||
|
|
||||||
|
**欢迎邮件规范**(联系人有邮箱时自动发送):
|
||||||
|
|
||||||
|
| 字段 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 主题 | 【房睿平台】您的账号已开通,欢迎登录 |
|
||||||
|
| 收件人 | 租户联系人邮箱 |
|
||||||
|
| 正文 | 公司名称、**Tenant Code**、登录地址、Tenant Admin 手机号(脱敏后三位)、**系统初始密码**(明文,首次登录后强制修改)、客服联系方式 |
|
||||||
|
| 备注 | 初始密码由系统随机生成(12 位,含大小写字母+数字),发送后立即标记为「首次登录强制修改」 |
|
||||||
|
|
||||||
|
**入驻信息 PDF**(无邮箱客户的备选方案):
|
||||||
|
- 包含与欢迎邮件相同的关键信息
|
||||||
|
- 入口:租户详情 → 基本信息 Tab → 「下载入驻信息」按钮
|
||||||
|
- 文件名:`{公司名称}_入驻信息_{日期}.pdf`
|
||||||
|
|
||||||
|
#### PA-002 Story A2:挂起问题租户
|
||||||
|
|
||||||
|
> 作为运营人员,我希望快速冻结欠费租户的访问,同时保证数据不丢失,并在欠费解决后一键恢复。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 挂起后,该租户所有用户登录跳转至「账号已暂停」提示页;平台后台对该租户数据访问不受影响
|
||||||
|
- [ ] 支持设置到期时间,到期后系统自动恢复租户状态,并发送通知邮件
|
||||||
|
- [ ] 所有挂起 / 恢复操作记录于审计日志,包含操作人、时间、原因
|
||||||
|
|
||||||
|
#### PA-003 Story A3:License 时效管理与到期自动挂起
|
||||||
|
|
||||||
|
> 作为运营人员(或 Platform Admin),我希望每个租户能设置 License 有效期,到期后系统自动挂起租户,并在到期前提前预警。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 每个租户「基本信息」中包含 **License 到期日期**字段,由 Platform Admin 在创建或续费时设定
|
||||||
|
- [ ] 系统每日自动检查 License 到期,到期后自动挂起,挂起原因标注为「License 到期」
|
||||||
|
- [ ] 自动挂起后,租户联系人收到通知邮件;Platform Admin 续费后可手动解除挂起
|
||||||
|
- [ ] **提前 15 天预警**:租户内 Tenant Admin 登录后管理界面顶部出现倒计时横幅:「您的 License 将于 X 天后到期({到期日期}),请联系平台续费」
|
||||||
|
- [ ] 倒计时横幅仅对 Tenant Admin 可见,不影响普通 Agent
|
||||||
|
- [ ] License 到期日期在租户列表与详情页均可见,支持「即将到期(15 天内)」筛选
|
||||||
|
|
||||||
|
#### PA-004 Story A4:查看与控制租户用户数(License 计费维度)
|
||||||
|
|
||||||
|
> 作为运营人员(或 Platform Admin),我希望看到每个租户的当前用户数,并能设置用户数上限,以便根据 License 授权进行管控。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 租户列表新增「当前用户数」列,显示该租户当前有效用户总数(含 Tenant Admin + 全部 Agent,不含已删除/离职)
|
||||||
|
- [ ] 租户详情页「基本信息」Tab 显示:当前用户数 / License 授权用户数上限(如:12 / 50)
|
||||||
|
- [ ] Platform Admin 可设置「License 授权用户数上限」字段;达到上限时,Tenant Admin 在该租户内无法继续创建新用户,并收到提示:「当前用户数已达 License 上限,请联系平台扩容」
|
||||||
|
- [ ] Platform Admin 可随时调整用户数上限(扩容 / 缩容),变更写入审计日志
|
||||||
|
- [ ] 租户列表支持按「用户数已满(≥ 上限)」筛选,便于平台运营主动识别需续费的租户
|
||||||
|
|
||||||
|
> **PA-011 Story A5:响应客户数据导出请求** — **状态**:暂缓(v1 不实现,列入后续迭代)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Persona E — Platform Admin David(租户注销合规流程)
|
||||||
|
|
||||||
|
> 处理租户主动申请注销或因严重违规被强制注销的场景。注销操作不可逆(硬删除后),需要多重防护——数据导出存证、双人复核、30 天冷静期撤销窗口。
|
||||||
|
|
||||||
|
#### PA-011 Story E1:租户注销 SOP(Tenant Cancellation SOP)
|
||||||
|
|
||||||
|
> 作为 Platform Admin,我希望执行租户注销时有标准化的审批流程保障——要求数据导出完成、双人复核授权、30 天冷静期可撤销——防止因误操作或内部滥权导致客户数据不可恢复地丢失。
|
||||||
|
|
||||||
|
**验收标准(AC)**:
|
||||||
|
|
||||||
|
**AC-1 发起注销申请**
|
||||||
|
- [ ] 运营人员或 Platform Admin 可在租户详情页发起「注销申请」,填写:注销原因(客户主动申请 / 严重违规 / 其他)、申请说明(自由文本)
|
||||||
|
- [ ] 发起后,租户状态流转至 `Pending Cancellation`(待审批注销),此时租户访问被阻断(等同挂起效果),不可通过常规「恢复」操作解除
|
||||||
|
- [ ] 发起操作写入审计日志
|
||||||
|
|
||||||
|
**AC-2 数据导出前置确认**
|
||||||
|
- [ ] 进入审批流程前,系统强制展示「数据导出确认」步骤:要求操作人触发该租户的完整数据导出(§5.1.5),等待导出任务完成(状态 Done)后方可提交审批
|
||||||
|
- [ ] 若申请人选择「客户已自行备份,无需平台导出」,需勾选明确确认框,并填写备注(客户签字回执编号或说明),此记录写入审计日志
|
||||||
|
- [ ] 审批单中需展示:导出任务 ID(或「已确认客户自备份」备注)、导出完成时间
|
||||||
|
|
||||||
|
**AC-3 双人复核审批**
|
||||||
|
- [ ] 注销审批流需要第二名 Platform Admin(不得与申请人为同一账号)在系统内确认审批,确认时需通过 MFA 二次验证
|
||||||
|
- [ ] 审批人可「批准」或「驳回」:驳回时必须填写驳回原因;驳回后租户状态恢复至 `Suspended`(仍不可直接恢复为 Active,需运营人员手动解除挂起)
|
||||||
|
- [ ] 若 48 小时内无人审批,系统向所有 Platform Admin 发送催审通知(站内消息 + 邮件)
|
||||||
|
|
||||||
|
**AC-4 软删除与 30 天冷静期**
|
||||||
|
- [ ] 审批通过后,租户立即进入软删除状态(`Pending Delete`,现有机制);冷静期默认 30 天
|
||||||
|
- [ ] 冷静期内,Platform Admin 可在租户列表「待清除」筛选下找到该租户,执行「撤销注销」
|
||||||
|
- [ ] 撤销注销需第二名 Platform Admin 同样通过 MFA 确认(与初始注销审批相同的双人复核机制)
|
||||||
|
- [ ] 撤销成功后,租户状态恢复至 `Suspended`(需运营人员手动评估后恢复为 Active);撤销操作及原因写入审计日志
|
||||||
|
|
||||||
|
**AC-5 硬删除与资源释放**
|
||||||
|
- [ ] 冷静期届满后,系统自动触发硬删除,或 Platform Admin 可在冷静期内提前手动触发硬删除(需再次 MFA 确认,并再次展示「此操作不可逆」警告)
|
||||||
|
- [ ] 硬删除释放:PostgreSQL Schema、Cloudflare R2 存储桶、Tenant Code、License 席位
|
||||||
|
- [ ] 硬删除完成后:向租户联系邮箱发送「账号已注销」通知(含注销日期、数据保留期说明);审计日志记录完整操作链
|
||||||
|
|
||||||
|
**注销状态流转**(补充至 §7 状态机):
|
||||||
|
```
|
||||||
|
[活跃 Active] 或 [已挂起 Suspended]
|
||||||
|
↓ 运营/Platform Admin 发起注销申请
|
||||||
|
[待审批注销 Pending Cancellation]
|
||||||
|
↓ 双人 MFA 审批通过
|
||||||
|
[待清除 Pending Delete](冷静期 30 天)
|
||||||
|
↓ 可在冷静期内由双人 MFA 撤销 → 恢复至 [已挂起 Suspended]
|
||||||
|
↓ 冷静期届满 或 提前手动硬删除(MFA)
|
||||||
|
[已删除 Deleted]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persona B — Platform Admin David(系统升级、回滚与版本治理)
|
||||||
|
|
||||||
|
> 负责平台技术运维,周期性执行版本升级,关注升级稳定性与租户影响面。拥有所有高危操作权限。
|
||||||
|
|
||||||
|
#### PA-005 Story B1:灰度系统升级
|
||||||
|
|
||||||
|
> 作为 Platform Admin,我希望先对内测租户升级新版本,验证稳定后再全量推送,避免一次性影响所有客户。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 升级前自动执行健康检查,存在异常服务时阻断升级并提示
|
||||||
|
- [ ] 支持指定目标租户进行灰度升级,灰度租户名单可编辑
|
||||||
|
- [ ] 升级过程实时展示进度(每个租户的升级状态),支持查看升级日志
|
||||||
|
- [ ] 升级失败时系统自动告警,并提供一键回滚入口
|
||||||
|
|
||||||
|
#### PA-006 Story B2:升级失败回滚
|
||||||
|
|
||||||
|
> 作为 Platform Admin,我希望在升级出现问题时立即回滚至上一稳定版本,并生成事件报告。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 回滚操作触发前自动保存当前状态快照
|
||||||
|
- [ ] 支持全量回滚或单租户回滚
|
||||||
|
- [ ] 回滚完成后生成事件报告:失败原因、回滚耗时、影响范围
|
||||||
|
- [ ] 回滚操作需二次身份验证确认(MFA)
|
||||||
|
|
||||||
|
#### PA-007 Story B3:查看平台与租户版本总览
|
||||||
|
|
||||||
|
> 作为 Platform Admin,我希望在管理界面一眼看到整个平台的版本情况,包括基础数据版本和每个租户各自的数据升级版本。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
|
||||||
|
「版本总览」页面分两部分:
|
||||||
|
|
||||||
|
- **Part 1:平台基础数据版本**
|
||||||
|
- [ ] 展示当前平台基础数据(公共 Schema 中的 PermissionDef、系统配置等 seed 数据)的版本号
|
||||||
|
- [ ] 该版本对所有租户一致,每次平台升级为一次性全量升级
|
||||||
|
- [ ] 字段:版本号、最后升级时间、升级描述、升级执行人
|
||||||
|
|
||||||
|
- **Part 2:租户数据升级版本**
|
||||||
|
- [ ] 列表展示每个租户当前数据版本号(即该租户 Schema 已完成的 migration 版本)
|
||||||
|
- [ ] 灰度升级下各租户版本可能不一致
|
||||||
|
- [ ] 字段:租户名称、当前数据版本、上次升级时间、升级状态(最新 / 待升级 / 升级中 / 升级失败)
|
||||||
|
- [ ] 支持按「待升级」「升级失败」筛选,快速定位异常租户
|
||||||
|
- [ ] 点击租户行可跳转至该租户详情的「备份记录」Tab
|
||||||
|
|
||||||
|
- [ ] 页面支持手动刷新;版本数据允许最多 5 分钟缓存延迟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Persona C — Platform Admin David(客户端发布治理)
|
||||||
|
|
||||||
|
> 同 Persona B,但聚焦桌面客户端版本上线、强制升级与跨租户版本分布。
|
||||||
|
|
||||||
|
#### PA-008 Story C1:发布新版本客户端
|
||||||
|
|
||||||
|
> 作为 Platform Admin,我希望通过本后台上传新版客户端安装包并配置版本信息,使全平台所有租户的客户端能感知到更新并引导升级。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 平台管理后台提供「客户端版本管理」页面(位于本后台一级菜单,与租户系统管理隔离)
|
||||||
|
- [ ] 支持上传 `.exe` 安装包,并填写:
|
||||||
|
- 版本号(SemVer:`X.Y.Z`,自动校验格式)
|
||||||
|
- 版本类型(普通更新 / 强制更新)
|
||||||
|
- 最低兼容版本(低于此版本的客户端将被强制升级)
|
||||||
|
- 更新日志(Markdown,最多 2000 字,对外展示)
|
||||||
|
- 内部发布说明(不对外)
|
||||||
|
- [ ] 支持版本状态:草稿(不对外生效)/ 已发布 / 已下线
|
||||||
|
- [ ] 上传成功后,系统自动计算 SHA256 校验值并填充到该版本元数据,作为客户端下载完成后的完整性校验依据
|
||||||
|
- [ ] 发布后,客户端下次检测时即可感知(无需等待)
|
||||||
|
- [ ] 支持版本回滚:将指定历史版本重新置为「已发布」,自动将当前版本标记为「已下线」
|
||||||
|
- [ ] 支持便携版(Portable ZIP)作为可选上传项
|
||||||
|
|
||||||
|
#### PA-009 Story C2:跨租户监控客户端版本分布
|
||||||
|
|
||||||
|
> 作为 Platform Admin,我希望跨租户查看当前所有在线客户端的版本分布,以了解全平台升级覆盖率,对仍使用旧版本的客户端发出提醒或强制升级。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 「客户端版本管理」页展示版本分布统计:各版本在线客户端数量及占比(饼图或条形图)
|
||||||
|
- [ ] 支持按租户维度查看版本分布(区分不同经纪公司,便于识别落后租户)
|
||||||
|
- [ ] 支持按租户统计安装数:可查看任一租户的「当前活跃安装数(最近 24h)」与「历史装机总数(不论是否活跃)」
|
||||||
|
- [ ] 支持「全平台租户活跃榜」视图:以列表形式展示各 active 状态租户的 `tenant_code`、租户名称、活跃安装数、历史装机总数,按活跃安装数降序排列
|
||||||
|
- [ ] 支持对指定版本范围的客户端推送「强制更新」标记(如:将所有低于 v1.5.0 的客户端标记为强制更新)
|
||||||
|
- [ ] 支持升级进度趋势图:新版本发布后各天累计升级完成的用户比例
|
||||||
|
|
||||||
|
> **客户端侧用户故事不在本 PRD 范围**(如「Agent 下载安装客户端」「Agent 自动升级」)。这些故事面向终端用户使用的桌面 App,不属于平台管理后台职责。但本 PRD 通过 Story C1/C2 为客户端侧故事提供版本数据源与控制能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Persona D — 只读审计员 Carol(合规审计)
|
||||||
|
|
||||||
|
> 负责平台合规审查,定期导出操作日志供法务或客户审查。无任何写权限。
|
||||||
|
|
||||||
|
#### PA-010 Story D1:审计日志查询与导出
|
||||||
|
|
||||||
|
> 作为审计员,我希望按操作人、时间范围、操作类型筛选操作日志,并导出为报告。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 日志列表支持多维度筛选:操作人、时间范围、操作对象、操作类型(创建 / 修改 / 删除 / 高危)
|
||||||
|
- [ ] 每条日志包含:操作人、操作时间、操作对象(租户/用户ID)、内容摘要、结果(成功/失败)、来源 IP
|
||||||
|
- [ ] 支持导出筛选结果为 CSV
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 功能详细说明
|
||||||
|
|
||||||
|
### 5.1 租户与 License 管理
|
||||||
|
|
||||||
|
#### 5.1.1 租户生命周期
|
||||||
|
|
||||||
|
**新建租户表单字段**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 公司名称 | Text | ✅ | 最大 100 字符 |
|
||||||
|
| 联系人 | Text | ✅ | 主联系人姓名 |
|
||||||
|
| 联系人手机号 | Text | ✅ | 用于创建初始 Tenant Admin |
|
||||||
|
| 联系邮箱 | Email | ❌(可选) | 发送欢迎邮件;无邮箱改用 PDF 下载 |
|
||||||
|
| 所在地区 | Select | ✅ | 省市两级 |
|
||||||
|
| 订阅套餐 | Select | ✅ | Basic / Professional / Enterprise |
|
||||||
|
| License 到期日期 | Date | ✅ | 到期后自动挂起 |
|
||||||
|
| License 授权用户数上限 | Number | ✅ | 用户数计费维度 |
|
||||||
|
|
||||||
|
**创建流程**:
|
||||||
|
1. 表单校验通过后,后台异步任务执行:
|
||||||
|
- 创建 PostgreSQL Schema(`tenant_{id}`)
|
||||||
|
- 执行 Migrate 初始化表结构
|
||||||
|
- 注入默认配置(见下方「默认配置内容」)
|
||||||
|
- 生成初始密码(12 位随机),标记为「首次登录强制修改」
|
||||||
|
- 联系邮箱已填写时:发送欢迎邮件(含 Tenant Code、初始密码、平台访问链接)
|
||||||
|
2. 任务完成后更新租户状态为 `active`;失败则全量回滚并标记为 `failed`
|
||||||
|
3. 生成唯一 Tenant ID(UUID)及 Tenant Code(12位纯数字,如 `202605023765`)
|
||||||
|
|
||||||
|
#### 5.1.2 默认配置内容(Schema 初始化时注入)
|
||||||
|
|
||||||
|
| 配置类型 | 内容 | 说明 |
|
||||||
|
|---------|------|------|
|
||||||
|
| 权限定义(PermissionDef) | 平台全量权限码 | 从 public schema 同步至租户 schema |
|
||||||
|
| 系统默认业务角色 | 7 个内置角色(置业顾问/店管/区管/区总/副总/总经/其他职能)+ 角色—权限绑定 | 按「角色权限矩阵.md」 |
|
||||||
|
| 初始 Tenant Admin 用户 | 以联系人手机号创建 | 写入「租户管理员」专属权限集合,独立于 7 个业务角色 |
|
||||||
|
|
||||||
|
#### 5.1.3 挂起(Suspend)
|
||||||
|
|
||||||
|
- **触发方式**:
|
||||||
|
- 手动:运营人员选择挂起原因(欠费 / 违规 / 主动申请 / 其他)
|
||||||
|
- 自动:系统每日检查 License 到期日期,到期自动挂起(原因「License 到期」)
|
||||||
|
- 可设置挂起到期时间(留空表示永久挂起直至手动恢复)
|
||||||
|
- 挂起效果:租户用户访问被拒,重定向至暂停提示页;平台管理后台对该租户数据访问不受影响
|
||||||
|
- 自动恢复:手动设置了到期时间的挂起会到期自动恢复;License 到期挂起须 Platform Admin 手动恢复
|
||||||
|
- 通知:挂起 / 恢复均向租户联系邮箱发送通知(无邮箱跳过)
|
||||||
|
- License 到期前 15 天起,租户端 Tenant Admin 看到倒计时横幅
|
||||||
|
|
||||||
|
#### 5.1.4 删除(Delete)
|
||||||
|
|
||||||
|
> **注意**:租户注销是高危操作,须遵循「PA-011 租户注销 SOP」——发起注销申请 → 强制数据导出确认 → 双人 MFA 复核审批 → 软删除(30 天冷静期,可撤销)→ 自动或提前手动硬删除。**不允许绕过此 SOP 直接执行删除。**
|
||||||
|
|
||||||
|
| 模式 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 软删除(Pending Delete) | SOP 审批通过后自动进入;数据保留 30 天(默认,可配置)后自动清除 |
|
||||||
|
| 硬删除 | 冷静期内 Platform Admin 提前触发(需 MFA + 不可逆警告),或冷静期届满自动触发;仅 Platform Admin 可操作 |
|
||||||
|
|
||||||
|
**删除前置条件(PA-011 SOP 强制执行)**:
|
||||||
|
1. 发起申请时必须填写注销原因
|
||||||
|
2. 审批前强制完成数据导出确认(平台导出 Done 或客户自备份书面确认)
|
||||||
|
3. 须第二名 Platform Admin(≠ 申请人)MFA 审批通过
|
||||||
|
4. 软删除冷静期内可由双人 MFA 执行「撤销注销」
|
||||||
|
|
||||||
|
删除完成后释放:访问域名 / Tenant Code、Cloudflare R2 存储桶、License 席位
|
||||||
|
|
||||||
|
#### 5.1.5 数据导出(Export)
|
||||||
|
|
||||||
|
> Story A5(响应客户数据导出请求)暂缓;本节描述能力为运营内部数据核查使用。
|
||||||
|
|
||||||
|
- 触发:管理员手动,选择目标租户 + 模块 + 格式
|
||||||
|
- 异步执行,状态实时刷新(Pending → In Progress → Done / Failed)
|
||||||
|
- 内容:结构化数据(CSV / JSON / SQL Dump)+ 文件资产 URL 清单(**不打包文件实体**)
|
||||||
|
- 模块选项:客户数据 / 房源数据 / 交易记录 / 系统配置 / 全量
|
||||||
|
- 存储:压缩后存于 Cloudflare R2 临时目录,签名下载链接 24 小时有效
|
||||||
|
|
||||||
|
**文件资产导出处理规则**(v1 决策):
|
||||||
|
- R2 Bucket 配置为 public read,文件通过 CDN 持久 URL 形式内嵌于导出数据
|
||||||
|
- 账号未硬删除前,CDN URL 持续有效
|
||||||
|
- 迁移类需求(需要文件实体)走「完整备份」流程,不走「数据导出」
|
||||||
|
|
||||||
|
| 导出格式 | 图片字段示例 | 附件字段示例 |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| CSV | `photos` 列:多个 CDN URL 以英文分号分隔 | `attachments` 列:`文件名\|CDN URL` 以分号分隔 |
|
||||||
|
| JSON | `"photos": [{"url": "...", "filename": "...", "created_at": "..."}]` | `"attachments": [{"url": "...", "filename": "..."}]` |
|
||||||
|
| SQL Dump | 文件元数据表原样导出,`file_url` 字段为 CDN URL | 同左 |
|
||||||
|
|
||||||
|
导出包附 `README.txt` 说明文件资产链接策略。
|
||||||
|
|
||||||
|
**数据导出 vs 完整备份**:
|
||||||
|
|
||||||
|
| 维度 | 数据导出(Export) | 完整备份(Backup) |
|
||||||
|
|------|------------------|-----------------|
|
||||||
|
| 用途 | 合规审计、数据核查、业务分析 | 灾难恢复、租户迁移 |
|
||||||
|
| 文件资产 | CDN URL 清单 | 含 R2 文件实体 |
|
||||||
|
| 完成时间 | 分钟级 | 小时级 |
|
||||||
|
| 触发方式 | 运营人员手动 | 手动 / 系统自动(升级前) |
|
||||||
|
| 存储成本 | 极低 | 较高 |
|
||||||
|
|
||||||
|
#### 5.1.6 数据备份(Snapshot)
|
||||||
|
|
||||||
|
- 自动触发:升级前系统对参与租户全量备份
|
||||||
|
- 手动触发:管理员可在租户详情页发起
|
||||||
|
- 内容:数据库 Schema(pg_dump)+ R2 文件存储(附件、图片)
|
||||||
|
- 字段:备份时间、触发方式、备份大小、状态
|
||||||
|
- 保留策略:默认最近 10 个版本,可在全局配置中调整
|
||||||
|
- 存储:加密;目标可选(本地 / S3 / R2 / GCS)
|
||||||
|
|
||||||
|
#### 5.1.7 数据恢复(Restore)
|
||||||
|
|
||||||
|
恢复流程:
|
||||||
|
```
|
||||||
|
选择目标备份版本
|
||||||
|
→ 二次确认(显示当前数据版本将被覆盖)
|
||||||
|
→ 自动对当前数据生成临时快照
|
||||||
|
→ 租户切换为维护模式
|
||||||
|
→ 执行恢复
|
||||||
|
→ 自动恢复服务 → 生成恢复报告
|
||||||
|
```
|
||||||
|
|
||||||
|
报告字段:操作人、操作时间、恢复前/后版本、耗时、结果。
|
||||||
|
|
||||||
|
#### 5.1.8 套餐升级
|
||||||
|
|
||||||
|
- 升级路径:Basic → Professional → Enterprise
|
||||||
|
- 升级前展示差异对比表(功能项、用户数上限、存储空间、API 额度)
|
||||||
|
- 生效模式:立即 / 下一账期
|
||||||
|
- 升级前自动备份;升级失败可一键回滚
|
||||||
|
- 升级历史:时间、操作人、升级前后套餐
|
||||||
|
|
||||||
|
#### 5.1.9 租户用户与权限管理
|
||||||
|
|
||||||
|
**Tenant Admin 管理**:
|
||||||
|
- 每个租户可设置 1 至多名 Tenant Admin
|
||||||
|
- Platform Admin 可直接创建新用户并赋予 Tenant Admin,或从租户现有用户中指定
|
||||||
|
- 支持新增 / 替换 / 撤销
|
||||||
|
|
||||||
|
**密码重置**(针对租户内任意用户):
|
||||||
|
- 方式一:发送重置链接至注册邮箱(用户自助)
|
||||||
|
- 方式二:管理员直接设置临时密码(首次登录强制修改)
|
||||||
|
- 所有操作进审计日志
|
||||||
|
|
||||||
|
#### 5.1.10 租户监控与统计
|
||||||
|
|
||||||
|
**资源监控指标**:
|
||||||
|
|
||||||
|
| 指标 | 展示维度 |
|
||||||
|
|------|---------|
|
||||||
|
| CPU / 内存占用 | 实时折线图 |
|
||||||
|
| 存储用量 | 当前值 vs 套餐上限 |
|
||||||
|
| API 调用次数 | 当日 / 本月累计 |
|
||||||
|
| 活跃用户数 | 当日活跃 |
|
||||||
|
| 当日登录次数 | 累计折线图 |
|
||||||
|
| 异常请求数 | 4xx / 5xx 分类 |
|
||||||
|
| 慢查询数量 | > 500ms |
|
||||||
|
|
||||||
|
支持为每个指标配置阈值告警(邮件 / Webhook)。
|
||||||
|
|
||||||
|
**可用性 / SLA**:
|
||||||
|
- 服务可用率统计:日 / 周 / 月
|
||||||
|
- 故障事件记录:开始 / 恢复时间、持续时长、影响描述
|
||||||
|
- SLA 达标率报告,可导出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 平台版本治理
|
||||||
|
|
||||||
|
#### 5.2.1 系统升级流程
|
||||||
|
|
||||||
|
```
|
||||||
|
上传/拉取升级包
|
||||||
|
→ 自动健康检查(所有服务正常才允许)
|
||||||
|
→ 配置升级策略:全量 / 灰度(指定内测租户)
|
||||||
|
→ 升级前自动备份(参与本次升级的租户)
|
||||||
|
→ 执行升级
|
||||||
|
→ 实时展示升级进度(租户维度状态列表)
|
||||||
|
→ 完成通知(成功/失败详情)
|
||||||
|
```
|
||||||
|
|
||||||
|
**灰度升级策略**:
|
||||||
|
- 维护「内测租户组」列表,由 Platform Admin 配置
|
||||||
|
- 灰度阶段仅对内测租户升级,其余租户保持原版本
|
||||||
|
- 内测验证通过(手动确认)后,触发全量升级
|
||||||
|
|
||||||
|
#### 5.2.2 升级回滚
|
||||||
|
|
||||||
|
- 触发:手动 / 自动(监控检测到错误率超阈值)
|
||||||
|
- 范围:全量回滚 / 单租户回滚
|
||||||
|
- 前置:自动保存当前状态快照
|
||||||
|
- 后续:生成事件报告(失败原因、回滚耗时、受影响租户列表)
|
||||||
|
- 必需:MFA 二次验证
|
||||||
|
|
||||||
|
#### 5.2.3 定时备份策略
|
||||||
|
|
||||||
|
| 配置项 | 选项 |
|
||||||
|
|--------|------|
|
||||||
|
| 备份频率 | 每小时 / 每日 / 每周 |
|
||||||
|
| 执行时间窗口 | 默认每日 02:00 |
|
||||||
|
| 保留数量 | 最近 N 个版本(默认 10) |
|
||||||
|
| 存储目标 | 本地 / AWS S3 / Cloudflare R2 / GCS |
|
||||||
|
|
||||||
|
支持为单租户配置独立备份计划,覆盖全局策略。备份失败自动告警,支持手动重试。
|
||||||
|
|
||||||
|
#### 5.2.4 版本总览
|
||||||
|
|
||||||
|
见 Story B3 验收标准。该视图为运营提供「平台基础数据版本 + 各租户数据版本」双维度可见性,是灰度升级时代的版本治理核心入口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 客户端发布治理
|
||||||
|
|
||||||
|
> 本节面向 Persona C 的 Story C1 / C2。客户端本身的技术形态、自动更新机制、签名分发等实现细节属于技术方案范畴,请参见 `TECH_STACK/平台管理后台技术方案.md`。本 PRD 仅描述产品视角的业务能力。
|
||||||
|
|
||||||
|
#### 5.3.1 版本管理
|
||||||
|
|
||||||
|
平台管理后台提供独立的「客户端版本管理」一级页面,承担:
|
||||||
|
|
||||||
|
**版本列表展示字段**:
|
||||||
|
|
||||||
|
| 列 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 版本号 | SemVer 格式,如 `v1.2.3` |
|
||||||
|
| 版本类型 | 普通更新 / 强制更新(后者标红) |
|
||||||
|
| 状态 | 草稿 / 已发布(绿)/ 已下线(灰) |
|
||||||
|
| 发布时间 | 设为已发布的时间 |
|
||||||
|
| 下载量 | 该版本安装包被下载次数 |
|
||||||
|
| 操作 | 发布 / 下线 / 编辑 / 复制下载链接 |
|
||||||
|
|
||||||
|
**新增/编辑版本表单字段**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 版本号 | Text | ✅ | SemVer,自动校验 |
|
||||||
|
| 版本类型 | 单选 | ✅ | 普通更新 / 强制更新 |
|
||||||
|
| 最低兼容版本 | Text | ❌ | 低于该版本的客户端被强制升级 |
|
||||||
|
| 安装包(EXE) | File | ✅ | 上传至 R2,最大 500MB |
|
||||||
|
| 便携版(ZIP) | File | ❌ | 同上 |
|
||||||
|
| SHA256 校验值 | Text(自动) | ✅ | 上传后系统计算并填充 |
|
||||||
|
| 更新日志 | Markdown | ✅ | 对外展示,最多 2000 字 |
|
||||||
|
| 发布说明(内部) | Text | ❌ | 仅内部查看 |
|
||||||
|
| 状态 | 单选 | ✅ | 草稿 / 立即发布 |
|
||||||
|
|
||||||
|
#### 5.3.2 版本分布统计
|
||||||
|
|
||||||
|
| 图表 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 版本分布饼图 | 按客户端版本号统计当前活跃用户数量及占比 |
|
||||||
|
| 升级进度趋势图 | 新版本发布后各天累计升级完成的用户比例 |
|
||||||
|
| 租户版本明细 | 按租户展示其员工的客户端版本分布 |
|
||||||
|
| 全平台租户活跃榜 | `tenant_code` + 租户名称 + 活跃安装数(最近 24h) + 历史装机总数,按活跃安装数降序 |
|
||||||
|
|
||||||
|
底层数据由 `public.client_heartbeats` 表的 `tenant_id` 维度聚合提供。
|
||||||
|
|
||||||
|
#### 5.3.3 强制更新推送
|
||||||
|
|
||||||
|
- Platform Admin 可对指定版本范围的客户端打上「强制更新」标记(如所有低于 v1.5.0 的客户端强制升级)
|
||||||
|
- 客户端侧的强制更新行为(不展示「稍后提醒」、必须升级后方可继续使用)由客户端实现保障
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 平台管理后台总体规划
|
||||||
|
|
||||||
|
#### 5.4.1 页面清单与访问权限
|
||||||
|
|
||||||
|
> 以下为产品视角的页面清单。「页面路径」以业务可读形式表达,不约束技术实现(具体 URL 由技术方案决定)。所有页面均位于平台管理后台域,无访问权限的角色(Tenant Admin、Agent 及未登录用户)一律重定向到登录页或 403。
|
||||||
|
|
||||||
|
| # | 页面 | 路径(业务可读) | 访问权限 | 说明 |
|
||||||
|
| --- | ----------- | -------------------------------- | --------------------------------------- | --------------------------------- |
|
||||||
|
| 1 | 登录页 | `/admin/login` | 公开(仅平台管理员账号可登录) | 强制 MFA 配置 |
|
||||||
|
| 2 | 仪表盘 | `/admin` | 全部三类管理员 | 平台总览、健康度、近期告警、最近高危操作 |
|
||||||
|
| 3 | 租户列表 | `/admin/tenants` | Platform Admin / 运营人员(写)/ 审计员(只读) | 检索、筛选、快捷操作 |
|
||||||
|
| 4 | 新建租户 | `/admin/tenants/new` | Platform Admin / 运营人员 | — |
|
||||||
|
| 5 | 租户详情:基本信息 | `/admin/tenants/{id}` | 同租户列表 | 含 License、用户数、状态等编辑入口 |
|
||||||
|
| 6 | 租户详情:用户管理 | `/admin/tenants/{id}/users` | 同上 | Tenant Admin 列表、用户列表、密码重置 |
|
||||||
|
| 7 | 租户详情:套餐信息 | `/admin/tenants/{id}/plan` | 同上 | 套餐详情、用量、升级入口 |
|
||||||
|
| 8 | 租户详情:监控 | `/admin/tenants/{id}/monitoring` | 同上 | 资源使用、SLA |
|
||||||
|
| 9 | 租户详情:备份记录 | `/admin/tenants/{id}/backups` | Platform Admin(写)/ 运营人员(仅触发备份)/ 审计员(只读) | 备份列表、触发备份、恢复入口(仅 Platform Admin) |
|
||||||
|
| 10 | 租户详情:操作历史 | `/admin/tenants/{id}/history` | 全部三类 | 该租户相关所有管理员操作日志 |
|
||||||
|
| 11 | 系统版本管理 | `/admin/system/versions` | Platform Admin(写)/ 运营人员(只读)/ 审计员(只读) | 含「版本总览」(平台基础数据版本 + 各租户数据版本)、升级、回滚 |
|
||||||
|
| 12 | 备份管理 | `/admin/system/backups` | Platform Admin(写)/ 运营人员(触发)/ 审计员(只读) | 全局备份计划、备份任务列表、恢复入口 |
|
||||||
|
| 13 | 监控与告警 | `/admin/monitoring` | Platform Admin / 运营人员(写)/ 审计员(只读) | 全局监控图表、告警规则、告警历史 |
|
||||||
|
| 14 | **客户端版本管理** | `/admin/client-releases` | Platform Admin(写)/ 运营人员(只读)/ 审计员(只读) | 版本列表、发布、下线、回滚、版本分布、租户活跃榜 |
|
||||||
|
| 15 | 审计日志 | `/admin/audit-logs` | 全部三类(含审计员) | 多维度筛选、导出 CSV |
|
||||||
|
| 16 | 管理员设置 | `/admin/settings/admins` | 仅 Platform Admin | 管理员账号、角色、MFA、IP 白名单、登录会话 |
|
||||||
|
|
||||||
|
#### 5.4.2 页面间导航逻辑(用户故事视角)
|
||||||
|
|
||||||
|
- 用户访问任一受保护页面但未登录 → 跳转 `/admin/login`
|
||||||
|
- 登录成功且未配置 MFA → 强制跳转 MFA 配置向导后再进入仪表盘
|
||||||
|
- 仪表盘的「最近高危操作」区域,每条记录可点击跳转到「审计日志」并自动以该记录为筛选条件
|
||||||
|
- 仪表盘的「系统健康」区域,点击异常服务可跳转「监控与告警」对应租户/服务视图
|
||||||
|
- 租户列表点击「公司名称」→ 进入「租户详情:基本信息」(默认 Tab)
|
||||||
|
- 租户详情各 Tab 之间通过页内 Tab 切换,URL 同步变化(页面 5–10)
|
||||||
|
- 租户详情「备份记录」中点击「恢复」→ 出现二次确认弹窗 + MFA 验证(仅 Platform Admin 可见)
|
||||||
|
- 「系统版本管理」中点击某租户行 → 跳转到该租户的「备份记录」Tab
|
||||||
|
- 「客户端版本管理」中点击「全平台租户活跃榜」中的租户名称 → 跳转到该租户「基本信息」Tab
|
||||||
|
- 客户端版本「发布 / 下线 / 强制更新推送」操作 → 二次确认;强制更新推送额外要求 MFA
|
||||||
|
- 任何高危操作(删除/注销租户、数据恢复、系统回滚、客户端版本下线、强制更新推送)触发 MFA 二次确认弹窗
|
||||||
|
- 租户详情「基本信息」页面,Platform Admin / 运营人员可发起「注销申请」;状态为 `Pending Cancellation` 时,原「挂起/恢复」按钮隐藏,显示「查看注销审批单」入口
|
||||||
|
- 注销审批待办:系统向第二名 Platform Admin 发送站内通知;点击通知进入审批单详情,展示申请人、注销原因、数据导出确认记录,提供「批准(MFA)」/「驳回」操作
|
||||||
|
- 审批批准后,租户自动进入 `Pending Delete`;租户列表「待清除」筛选下可见,提供「撤销注销(双人 MFA)」和「提前硬删除(MFA)」入口
|
||||||
|
- 用户超过 30 分钟无操作 → 自动登出,下次操作跳转登录页
|
||||||
|
- 「管理员设置」中 Platform Admin 强制登出某管理员 → 该管理员的所有会话立即失效,下次请求跳转登录页
|
||||||
|
|
||||||
|
#### 5.4.3 仪表盘内容
|
||||||
|
|
||||||
|
| 模块 | 展示内容 |
|
||||||
|
|------|---------|
|
||||||
|
| 全局概览 | 总租户数、活跃租户数、本月新增 |
|
||||||
|
| 系统健康 | 各核心服务状态(Django / PostgreSQL / Redis / Celery / R2) |
|
||||||
|
| 近期告警 | 最近 24 小时告警,按严重程度分类 |
|
||||||
|
| 资源概览 | 平台整体存储用量、API 调用量趋势 |
|
||||||
|
| 客户端覆盖 | 当前活跃客户端总数、最新版本占比 |
|
||||||
|
| 最近操作 | 最近 10 条高危操作审计记录 |
|
||||||
|
|
||||||
|
#### 5.4.4 租户列表
|
||||||
|
|
||||||
|
- 分页(默认 20 条/页)
|
||||||
|
- 搜索:公司名称、Tenant Code、联系邮箱关键词
|
||||||
|
- 筛选:状态(Active / Suspended / Pending Cancellation / Pending Delete / Deleted)、套餐、注册时间、**即将到期(15 天内)**、**用户数已满**
|
||||||
|
- 列:公司名称、Tenant Code、套餐、状态、注册时间、活跃用户数 / License 上限、客户端最新版本占比
|
||||||
|
- 快捷操作:查看详情、挂起、发起备份、数据导出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 业务 API 操作清单(产品视角)
|
||||||
|
|
||||||
|
> 仅描述「平台管理员需要能完成哪些业务操作」;不定义具体 RESTful 路径、HTTP 方法、参数结构。具体接口设计见对应技术方案文档。
|
||||||
|
|
||||||
|
#### 5.5.1 租户管理
|
||||||
|
|
||||||
|
- 平台管理员需要能查询租户列表(支持多维度筛选)
|
||||||
|
- 平台管理员需要能查看单租户的完整详情(基本信息、套餐、用户、License、备份、操作历史)
|
||||||
|
- 平台管理员需要能创建新租户(含异步初始化 Schema 与默认配置)
|
||||||
|
- 平台管理员需要能编辑租户基本信息(公司名、联系人、邮箱、地区)
|
||||||
|
- 平台管理员需要能挂起 / 恢复租户(含挂起原因、到期时间)
|
||||||
|
- 平台管理员(运营人员)需要能发起租户注销申请(PA-011 SOP:填写原因、完成数据导出确认)
|
||||||
|
- 第二名 Platform Admin 需要能审批租户注销申请(批准需 MFA;驳回需填写驳回原因)
|
||||||
|
- 平台管理员需要能在冷静期内撤销租户注销(双人 MFA)
|
||||||
|
- 平台管理员需要能在冷静期内提前手动触发硬删除(MFA + 不可逆确认)
|
||||||
|
- 系统需能在冷静期届满后自动触发硬删除并释放所有资源
|
||||||
|
- 平台管理员需要能软删除 / 硬删除租户(仅限通过 PA-011 SOP 触发,不提供绕过 SOP 的直接删除入口)
|
||||||
|
- 平台管理员需要能调整租户的 License 到期日期
|
||||||
|
- 平台管理员需要能调整租户的 License 授权用户数上限
|
||||||
|
- 平台管理员需要能升级租户套餐
|
||||||
|
- 平台管理员需要能下载租户的「入驻信息 PDF」
|
||||||
|
|
||||||
|
#### 5.5.2 租户用户管理
|
||||||
|
|
||||||
|
- 平台管理员需要能查询租户内的全部用户
|
||||||
|
- 平台管理员需要能新增 / 替换 / 撤销 Tenant Admin
|
||||||
|
- 平台管理员需要能为租户内任意用户重置密码(链接 / 临时密码两种方式)
|
||||||
|
|
||||||
|
#### 5.5.3 数据导出与备份
|
||||||
|
|
||||||
|
- 平台管理员需要能为指定租户触发数据导出(异步,可查询任务状态、获取下载链接)
|
||||||
|
- 平台管理员需要能为指定租户手动触发完整备份
|
||||||
|
- 平台管理员需要能查询某租户的备份记录列表
|
||||||
|
- 平台管理员需要能基于某个备份执行恢复操作(需 MFA)
|
||||||
|
- 平台管理员需要能配置全局 / 单租户的定时备份策略
|
||||||
|
|
||||||
|
#### 5.5.4 平台版本与升级
|
||||||
|
|
||||||
|
- 平台管理员需要能上传 / 拉取系统升级包
|
||||||
|
- 平台管理员需要能配置升级策略(全量 / 灰度,含内测租户名单)
|
||||||
|
- 平台管理员需要能触发升级,并实时查询升级进度(租户维度)
|
||||||
|
- 平台管理员需要能查看升级日志
|
||||||
|
- 平台管理员需要能触发升级回滚(全量 / 单租户,需 MFA)
|
||||||
|
- 平台管理员需要能查询「平台基础数据版本」与「各租户数据升级版本」总览
|
||||||
|
|
||||||
|
#### 5.5.5 客户端发布
|
||||||
|
|
||||||
|
- 平台管理员需要能查询客户端版本列表(含状态、下载量)
|
||||||
|
- 平台管理员需要能新增客户端版本(上传安装包,系统自动计算 SHA256)
|
||||||
|
- 平台管理员需要能修改某版本元数据(更新日志、版本类型、最低兼容版本等)
|
||||||
|
- 平台管理员需要能切换某版本状态(草稿 → 已发布 / 已发布 → 已下线)
|
||||||
|
- 平台管理员需要能将某历史版本回滚为「已发布」(同时把当前版本置为已下线)
|
||||||
|
- 平台管理员需要能对指定版本范围的客户端打上「强制更新」标记
|
||||||
|
- 平台管理员需要能查询全平台客户端版本分布(饼图、趋势图)
|
||||||
|
- 平台管理员需要能查询全平台租户活跃榜(活跃安装数、历史装机总数)
|
||||||
|
- 平台管理员需要能查询任一租户的客户端版本分布与活跃数
|
||||||
|
|
||||||
|
> 客户端**自身**与更新服务的交互(如查询最新版本、上报心跳)属于客户端运行时与平台之间的接口,不属于平台管理员的操作;本 PRD 不在此章描述,由 `TECH_STACK/平台管理后台技术方案.md` 定义。
|
||||||
|
|
||||||
|
#### 5.5.6 监控与告警
|
||||||
|
|
||||||
|
- 平台管理员需要能查询全局 / 单租户的监控图表
|
||||||
|
- 平台管理员需要能配置告警规则(指标 + 阈值 + 通知渠道)
|
||||||
|
- 平台管理员需要能查询告警历史
|
||||||
|
|
||||||
|
#### 5.5.7 审计日志
|
||||||
|
|
||||||
|
- 平台管理员(含审计员)需要能按多维度筛选审计日志
|
||||||
|
- 平台管理员(含审计员)需要能导出筛选结果为 CSV
|
||||||
|
|
||||||
|
#### 5.5.8 平台管理员账号与安全
|
||||||
|
|
||||||
|
- Platform Admin 需要能创建 / 编辑 / 停用平台管理员账号
|
||||||
|
- Platform Admin 需要能配置管理员角色(Platform Admin / 运营人员 / 只读审计员)
|
||||||
|
- 全部平台管理员需要能首次登录时配置 MFA(无法跳过)
|
||||||
|
- 全部平台管理员需要能在高危操作时通过 MFA 二次确认
|
||||||
|
- Platform Admin 需要能配置 IP 白名单
|
||||||
|
- Platform Admin 需要能查看活跃会话并强制登出指定管理员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 安全与访问控制
|
||||||
|
|
||||||
|
**强制要求(不可降级)**:
|
||||||
|
|
||||||
|
| 安全要求 | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| MFA 强制启用 | 所有管理员账号首次登录强制配置 TOTP;不可跳过 |
|
||||||
|
| IP 白名单 | 仅允许指定 IP 范围访问平台管理后台 |
|
||||||
|
| 高危操作二次验证 | 删除租户、数据恢复、系统回滚、客户端版本下线、强制更新推送均触发 MFA 二次确认 |
|
||||||
|
| 会话超时 | 无操作 30 分钟自动登出,Token 失效 |
|
||||||
|
| 强制登出 | Platform Admin 可在「管理员设置」中强制终止指定管理员的所有会话 |
|
||||||
|
| 与租户应用隔离 | 平台管理后台部署在独立平台域名,不与租户应用共享 Session / Cookie |
|
||||||
|
|
||||||
|
### 5.7 操作审计日志规范
|
||||||
|
|
||||||
|
所有写操作(Create / Update / Delete)及高危操作必须落审计日志,字段:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": "UUID",
|
||||||
|
"operator_id": "管理员用户 ID",
|
||||||
|
"operator_name": "管理员显示名",
|
||||||
|
"action_type": "CREATE_TENANT | SUSPEND_TENANT | RESUME_TENANT | DELETE_TENANT | HARD_DELETE_TENANT | RESTORE_DATA | SYSTEM_UPGRADE | ROLLBACK | RESET_PASSWORD | RELEASE_CLIENT_VERSION | OFFLINE_CLIENT_VERSION | FORCE_UPDATE_PUSH | UPDATE_LICENSE | UPDATE_LICENSE_USER_LIMIT | AUTO_SUSPEND_LICENSE_EXPIRED | ...",
|
||||||
|
"target_type": "Tenant | User | System | Backup | ClientRelease | Admin",
|
||||||
|
"target_id": "操作对象 ID",
|
||||||
|
"target_name": "操作对象可读名称",
|
||||||
|
"payload_summary": "操作内容摘要(非敏感字段)",
|
||||||
|
"result": "SUCCESS | FAILED",
|
||||||
|
"error_message": "失败原因(如有)",
|
||||||
|
"ip_address": "操作来源 IP",
|
||||||
|
"created_at": "ISO 8601 时间戳"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 角色权限矩阵
|
||||||
|
|
||||||
|
| 操作 | Platform Admin | 运营人员 | 只读审计员 |
|
||||||
|
|------|---------------|---------|-----------||
|
||||||
|
| 创建租户 | ✅ | ✅ | ❌ |
|
||||||
|
| 挂起 / 恢复租户 | ✅ | ✅ | ❌ |
|
||||||
|
| 发起租户注销申请(PA-011 SOP) | ✅ | ✅ | ❌ |
|
||||||
|
| 审批注销申请(第二人复核,MFA) | ✅ | ❌ | ❌ |
|
||||||
|
| 撤销注销(冷静期内,双人 MFA) | ✅ | ❌ | ❌ |
|
||||||
|
| 提前手动硬删除(MFA) | ✅ | ❌ | ❌ |
|
||||||
|
| 软删除 → Pending Delete(仅经 SOP 自动触发) | 系统自动 | — | — |
|
||||||
|
| 调整 License 到期日期 / 用户数上限 | ✅ | ✅ | ❌ |
|
||||||
|
| 数据导出 | ✅ | ✅ | ❌ |
|
||||||
|
| 手动触发备份 | ✅ | ✅ | ❌ |
|
||||||
|
| 数据恢复 | ✅ | ❌ | ❌ |
|
||||||
|
| 系统升级 | ✅ | ❌ | ❌ |
|
||||||
|
| 系统回滚 | ✅ | ❌ | ❌ |
|
||||||
|
| 查看版本总览 | ✅ | ✅ | ❌ |
|
||||||
|
| 配置告警规则 | ✅ | ✅ | ❌ |
|
||||||
|
| 发布 / 下线客户端版本 | ✅ | ❌ | ❌ |
|
||||||
|
| 客户端版本回滚 | ✅ | ❌ | ❌ |
|
||||||
|
| 推送强制更新标记 | ✅ | ❌ | ❌ |
|
||||||
|
| 查看客户端版本分布与活跃榜 | ✅ | ✅ | ✅ |
|
||||||
|
| 查看审计日志 | ✅ | ✅ | ✅ |
|
||||||
|
| 导出审计日志 | ✅ | ✅ | ✅ |
|
||||||
|
| 管理员账号管理 | ✅ | ❌ | ❌ |
|
||||||
|
| 强制登出管理员 | ✅ | ❌ | ❌ |
|
||||||
|
| 配置 IP 白名单 | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 租户状态机
|
||||||
|
|
||||||
|
```
|
||||||
|
[新建中 Creating]
|
||||||
|
↓ 成功
|
||||||
|
[活跃 Active] ←──────────────────┐
|
||||||
|
↓ 手动 / License 到期挂起 │ 到期自动恢复(仅手动挂起且设置了到期时间)/ 手动恢复
|
||||||
|
[已挂起 Suspended] ───────────────┘
|
||||||
|
↓ 运营/PA 发起注销申请(PA-011)
|
||||||
|
[待审批注销 Pending Cancellation]
|
||||||
|
↓ 驳回(第二名 PA)→ [已挂起 Suspended]
|
||||||
|
↓ 批准(第二名 PA,MFA)
|
||||||
|
[待清除 Pending Delete](冷静期 30 天)
|
||||||
|
↓ 双人 MFA 撤销注销 → [已挂起 Suspended]
|
||||||
|
↓ 冷静期届满(自动)/ 提前手动硬删除(MFA)
|
||||||
|
[已删除 Deleted]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:Active / Suspended 状态下均可发起注销申请(后者更常见)。不允许直接跳过 SOP 从任何状态进入 Pending Delete 或 Deleted。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 不构建清单(What We're NOT Building)
|
||||||
|
|
||||||
|
| 请求/功能 | 原因 | 重新评估条件 |
|
||||||
|
|----------|------|------------|
|
||||||
|
| 自动化账单与发票 | 财务模块独立立项 | 财务模块 PRD 完成后接入 |
|
||||||
|
| 租户端自助迁移工具 | 当前规模不需要 | 租户数 > 500 时重新评估 |
|
||||||
|
| 平台管理后台移动端 | 运营场景明确为 PC | v2 规划,用户调研支持时推进 |
|
||||||
|
| Webhook 事件推送市场 | 集成复杂度高,无客户驱动 | 有 3+ 客户明确需求时评估 |
|
||||||
|
| 多语言管理界面 | 内部团队中文已满足 | 国际化扩张时规划 |
|
||||||
|
| macOS / Linux 客户端 | 99% 用户为 Windows | 市场需求出现时评估 |
|
||||||
|
| 移动端客户端 App | v2 规划 | v2 启动时 |
|
||||||
|
| 客户端离线模式 | 联网使用为既定形态 | 暂不考虑 |
|
||||||
|
| 客户端代码混淆 / 反逆向 | 优先功能交付 | 安全审计要求时启动 |
|
||||||
|
| 响应客户数据导出请求 | 暂缓 | 后续迭代排期时细化 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 发布计划
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 范围 | 通过标准 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 内部 Alpha | Week 1–4 | 平台内部团队 | 核心租户 CRUD 流程无 P0 Bug;MFA 可用;客户端发布流程闭环 |
|
||||||
|
| 封闭 Beta | Week 5–6 | 运营团队日常使用 + 1 家种子客户的客户端 | 备份/恢复完整可用;审计日志 100% 覆盖;客户端自动更新成功率 ≥ 95% |
|
||||||
|
| 正式上线 | Week 7 | 全量运营团队 + 全部客户的客户端 | 升级/回滚验证通过;监控告警规则配置完成;客户端版本一致性 ≥ 95% |
|
||||||
|
|
||||||
|
**回滚标准**:
|
||||||
|
- 正式上线 72 小时内发现租户数据隔离漏洞或审计日志丢失 → 立即回滚平台管理后台
|
||||||
|
- 客户端版本上线 24 小时内:自动更新失败率 > 5%、白屏 / 崩溃率 > 2%、或收到 P0 安全报告 → 立即下线该版本并恢复上一稳定版本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 技术考量(指引性)
|
||||||
|
|
||||||
|
> 本节为产品视角对技术决策的影响概述,详细方案请见各技术文档。
|
||||||
|
|
||||||
|
| 维度 | 关键依赖 | 备注 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 多租户隔离 | `django-tenants` | 平台管理后台数据落 `public` schema,跨租户查询走 `public` 维度 |
|
||||||
|
| 异步任务 | Celery + Celery Beat | 创建租户、备份、导出、定时挂起检查等 |
|
||||||
|
| 数据库备份 | PostgreSQL `pg_dump`(待评估流式方案) | 大租户备份耗时风险 |
|
||||||
|
| 文件存储 | Cloudflare R2 | 备份、导出包、客户端安装包 |
|
||||||
|
| 监控 | Sentry + Grafana | 已在技术栈中规划 |
|
||||||
|
| MFA | `django-otp` + TOTP | 待最终选型 |
|
||||||
|
| 客户端 | Electron + electron-updater | 见 `TECH_STACK/平台管理后台技术方案.md` |
|
||||||
|
| 客户端心跳/活跃统计 | `public.client_heartbeats`(Upsert + 24h 活跃口径,见 `ADR-20260430-007`) | 是租户活跃榜与版本分布的数据源 |
|
||||||
|
| 客户端 API 命名空间 | 统一 `/api/release/...`(见 `ADR-20260430-009`) | 与租户客源管理 `apps/client/` 命名空间隔离 |
|
||||||
|
|
||||||
|
### 待解决问题(开发启动前必须确认)
|
||||||
|
|
||||||
|
- [ ] 数据库备份方案:`pg_dump` 直接执行还是基于 WAL 的增量备份(如 pgBackRest)?— Owner: 工程负责人
|
||||||
|
- [ ] 监控数据来源:Grafana 直接对接 PostgreSQL 还是通过 Prometheus Exporter?— Owner: 运维团队
|
||||||
|
- [ ] MFA 库选型:`django-otp` + TOTP 还是集成第三方认证?— Owner: 工程负责人
|
||||||
|
- [ ] 审计日志存储:写入 `public` schema 还是独立日志服务(如 Elasticsearch)?— Owner: 工程负责人
|
||||||
|
- [ ] 客户端 EV 代码签名证书采购主体与预算 — Owner: IT 负责人
|
||||||
|
- [ ] CI/CD 平台选型(客户端构建签名流水线)— Owner: 运维负责人
|
||||||
|
- [ ] 客户端便携版(Portable ZIP)是否纳入 v1 — Owner: PM
|
||||||
|
- [ ] 客户端启动时租户 URL 分发方式(统一域名重定向 vs 内置配置文件)— Owner: 产品 + 工程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 附录
|
||||||
|
|
||||||
|
### 11.1 术语表
|
||||||
|
|
||||||
|
| 术语 | 定义 |
|
||||||
|
|------|------|
|
||||||
|
| Platform Admin | 平台超级管理员,拥有所有高危操作权限 |
|
||||||
|
| 运营人员 | 平台日常运营,承担租户管理、监控巡检 |
|
||||||
|
| 只读审计员 | 仅查询与导出审计日志,无写权限 |
|
||||||
|
| Tenant Admin | 租户内的最高权限用户,**对平台管理后台无访问权限** |
|
||||||
|
| Tenant Code | 租户唯一可读短码,用作租户访问 URL 中的 `tenant=` 参数 |
|
||||||
|
| 灰度升级 | 先对内测租户升级、验证通过后再全量推送 |
|
||||||
|
| 数据导出 | 输出结构化数据 + 文件 CDN URL 清单,分钟级,不含文件实体 |
|
||||||
|
| 完整备份 | 数据库 + 文件实体的完整副本,小时级,用于灾难恢复与租户迁移 |
|
||||||
|
| SemVer | 语义化版本控制 `主.次.补丁` |
|
||||||
|
| EV 证书 | Extended Validation 代码签名证书 |
|
||||||
|
| SHA256 | 用于校验客户端安装包完整性的散列算法 |
|
||||||
|
|
||||||
|
### 11.2 关联文档
|
||||||
|
|
||||||
|
| 类型 | 文档 |
|
||||||
|
| ------------------ | ----------------------------------------- |
|
||||||
|
| 项目入口 | `README.md` |
|
||||||
|
| 开发约束 | `AGENTS.md` |
|
||||||
|
| MVP 范围 | `PRD/PRD_MVP.md` |
|
||||||
|
| 客户端技术方案 | `TECH_STACK/平台管理后台技术方案.md` |
|
||||||
|
| Public Schema 数据模型 | `DATA_MODEL/DATA_MODEL_PUBLIC.md` |
|
||||||
|
| ADR | `ADR.md`(含 `ADR-20260502-001`:本 PRD 合并决策) |
|
||||||
1889
Project/fonrey/PRD/房源管理/房源管理模块PRD.md
Normal file
711
Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
# PRD: 楼盘管理模块
|
||||||
|
**状态**: Draft
|
||||||
|
**作者**: 产品经理
|
||||||
|
**最后更新**: 2026-04-23(v1.0 初稿,基于楼盘管理列表、楼盘详情(楼盘信息/楼栋管理/结构管理/楼盘照片/楼盘价格走势/周边配套)、区域管理(城区/商圈/关联关系)、学校管理共14张截图分析完成)
|
||||||
|
**版本**: 1.0
|
||||||
|
**所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
**关联模块**: 房源管理、客源管理、组织人事管理、权限管理
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 作者 | 变更说明 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| v1.0 | 2026-04-23 | 产品经理 | 初稿,基于楼盘管理列表、楼盘详情(楼盘信息/楼栋管理/结构管理/楼盘照片/楼盘价格走势/周边配套)、区域管理(城区/商圈/关联关系)、学校管理共 14 张截图分析完成 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题陈述
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
|
||||||
|
楼盘(小区)是房源管理的基础数据底座。一套房源必须归属于某一楼盘,楼盘的信息完整度直接决定房源数据的质量、搜索的准确性,以及向买客推荐时的可信度。
|
||||||
|
|
||||||
|
现实业务中,楼盘数据的核心痛点集中在以下几个方面:
|
||||||
|
|
||||||
|
- **数据分散不统一**:楼盘名称存在多版本叫法(标准名、别名、营销名等),各门店录入口径不一,导致同一楼盘在系统中多次重复存在,房源匹配困难
|
||||||
|
- **楼栋/单元/房号缺失**:房源录入时无法关联准确的楼栋结构,导致"同一门牌号多套房源"的数据混乱问题
|
||||||
|
- **区域体系不规范**:城区-商圈两级区域结构缺乏统一维护,不同城市、分公司之间区域命名各行其是,无法支持跨区域数据汇总
|
||||||
|
- **学区信息不完整**:学区是买家购房的核心关注点,但缺乏与楼盘关联的学区数据库,全靠经纪人口述,准确性和一致性极差
|
||||||
|
- **价格走势不可视**:缺少楼盘级别的历史成交价和挂牌价趋势数据,经纪人在客户询价时无参考依据,议价能力弱
|
||||||
|
|
||||||
|
### 目标用户
|
||||||
|
|
||||||
|
| 角色 | 描述 | 使用频率 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 运营/数据管理员 | 维护楼盘信息、楼栋结构、区域体系、学校信息的标准化数据 | 每日 |
|
||||||
|
| Agent(经纪人) | 查询楼盘详情、参考价格走势、了解周边配套辅助成交 | 每日 |
|
||||||
|
| 店长/经理 | 监控楼盘数据完整度,分析区域市场行情 | 每周 |
|
||||||
|
| Tenant Admin(租户管理员) | 配置区域关联关系,管理数据标准 | 不定期 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目标与成功指标
|
||||||
|
|
||||||
|
| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 |
|
||||||
|
|------|------|----------|--------|----------|
|
||||||
|
| 提升楼盘数据完整度 | 楼盘及单元完整率 | 待统计 | ≥ 95% | 上线后 90 天 |
|
||||||
|
| 减少重复楼盘 | 楼盘关联房号率 | 待统计 | ≥ 90% | 上线后 90 天 |
|
||||||
|
| 提升学区信息准确率 | 有学区关联的楼盘占比 | 待统计 | ≥ 80% | 上线后 60 天 |
|
||||||
|
| 提升区域数据规范度 | 有坐标的商圈占比 | 1.83%(截图数据) | ≥ 90% | 上线后 120 天 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 非目标(本期不做)
|
||||||
|
|
||||||
|
- **应用数据标准**:数据标准应用功能本期不做,后续版本规划
|
||||||
|
- 不包含楼盘的对外门户网站展示(楼盘详情页对客展示为营销模块)
|
||||||
|
- 不包含楼盘数据与第三方平台(链家、贝壳等)的数据同步集成
|
||||||
|
- 不包含销控盘(新房/一手楼盘)功能,本模块聚焦二手房楼盘管理
|
||||||
|
- 不包含楼盘的 AI 自动补全/抓取功能(数据采集为独立项目)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 用户故事与验收标准
|
||||||
|
|
||||||
|
### Story 1:运营人员在楼盘列表中查找并管理楼盘
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 通过楼盘列表快速查找特定楼盘并了解其数据完整度状态,**So that** 可以有针对性地补全数据,提升整体楼盘数据质量。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 楼盘列表支持关键词搜索:楼盘名称/别名/概要地址,点击"查询"触发搜索,支持"清除"重置
|
||||||
|
- [ ] 支持按区域过滤(行政区多选):不限 / 静安 / 闵行 / 普陀 / 松江 / 长宁 等
|
||||||
|
- [ ] 支持按用途过滤:不限 / 住宅 / 别墅 / 商住 / 商业 / 写字楼 / 其他
|
||||||
|
- [ ] 支持按"固定情况"、"完善情况"、"楼盘类型"、"楼标号小区非标结构情况"、"有无房源"、"楼栋类型"、"权属关系"、"有无坐标"等维度组合筛选(下拉)
|
||||||
|
- [ ] 列表顶部实时显示数据完整度统计面板,包含:楼盘关联率、楼栋及单元完整率、房号匹配率、处置率、入住人结构数据、有效结构数量、房源对标等关键指标,并提供"重新计算"入口
|
||||||
|
- [ ] 列表字段包含:楼盘名称(含信息标签、标准楼盘入口、标准楼栋入口、标准房号入口)、楼盘类型、详细地址、城区商圈、当月挂牌均价(元/m²)、楼栋数、产品数、房源数(出售/出租/总计)
|
||||||
|
- [ ] 列表支持批量操作:批量新增楼栋、批改区域商圈、删除、合并楼盘
|
||||||
|
- [ ] 支持新增楼盘(主 CTA 按钮)
|
||||||
|
- [ ] 每行操作列提供"编辑"和"删除"按钮
|
||||||
|
- [ ] 列表底部支持分页(20条/页,支持跳页)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 2:运营人员查看和编辑楼盘基本信息
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在楼盘详情页查看完整的楼盘信息并快速修正错误,**So that** 保持楼盘档案的准确性,为房源信息提供可靠的数据基础。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 楼盘详情页顶部展示楼盘完整名称(含标准名+别名组合)及4类权限标签(楼栋锁、房号锁、信息锁、标准房号锁)
|
||||||
|
- [ ] 顶部提供"解锁楼盘"操作按钮(受权限控制)
|
||||||
|
- [ ] 详情页分 Tab 展示:楼盘信息 / 楼栋管理 / 结构管理 / 楼盘照片 / 楼盘附件 / 周边配套 / 楼盘价格走势 / 销控盘
|
||||||
|
- [ ] 楼盘信息 Tab 下,"基本信息"区块字段包含:
|
||||||
|
- 城区商圈、小区地址、概要地址、建筑类型
|
||||||
|
- 楼栋结构(枚举:单元-房号等)
|
||||||
|
- 小区别名(可多个)
|
||||||
|
- 土地使用年限(如:70年)
|
||||||
|
- 物业类型(住宅/别墅等)
|
||||||
|
- 权属类别(如:商品房住宅)
|
||||||
|
- 竣工年限、总户数、单元总数
|
||||||
|
- 小区坐标(经纬度,可点击坐标地图定位)
|
||||||
|
- [ ] 楼盘信息 Tab 下,"对口学校"区块展示关联学校列表,字段:学校名称/学校类型/学校性质/学校等级
|
||||||
|
- [ ] 楼盘信息 Tab 下,"其他信息"区块字段包含:
|
||||||
|
- 小区总建筑面积、小区占地面积、容积率、绿化率
|
||||||
|
- 物业公司、物业费(元/m²/月)、物业电话
|
||||||
|
- 开发商、车位数(总数)、车位数(地下)、停车位配比
|
||||||
|
- 供水类型、供电类型、统一供暖(有/无)、有无燃气
|
||||||
|
- 备注
|
||||||
|
- [ ] 点击"编辑"按钮跳转至独立编辑页面,编辑完成点击"确定"保存,"取消"不保存并返回
|
||||||
|
- [ ] 楼盘地址有误时提供"纠错"入口(页面顶部"楼盘地址有误?点此【纠错】")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 3:运营人员编辑楼盘信息(完整编辑页)
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在专用编辑页中全量修改楼盘的基本信息、学校信息和其他信息,**So that** 一次性完成楼盘档案的系统化维护。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
|
||||||
|
#### 基本信息区块
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 小区名称 | 文本输入 | 是 | 只读(灰底),不可在编辑页修改,需通过合并/申请流程处理 |
|
||||||
|
| 小区地址 | 文本输入 | 是 | 只读(灰底) |
|
||||||
|
| 物业类型 | 单选 | 是 | 已选中N个,可调整 |
|
||||||
|
| 城区商圈 | 级联下拉 | 否 | 城区 + 商圈二级联动 |
|
||||||
|
| 楼栋结构 | 下拉 | 是 | 单元-房号 / 其他等枚举 |
|
||||||
|
| 小区别名 | 文本 + 标准别名 | 否 | 支持自定义别名(最多20字,多个用「回号」分隔),系统别名只读展示 |
|
||||||
|
| 建筑类型 | 单选组 | 否 | 板楼 / 塔楼 / 板塔结合 |
|
||||||
|
| 概要地址 | 文本输入 | 否 | 简短描述,如"海波路1000弄" |
|
||||||
|
| 土地使用年限 | 下拉 | 否 | 已选中N个 |
|
||||||
|
| 竣工年限 | 多选下拉 | 否 | 已选中N个 |
|
||||||
|
| 权属类别 | 多选下拉 | 否 | 已选中N个 |
|
||||||
|
| 单元总数 | 数字输入 | 否 | 配合"栋"单位标识 |
|
||||||
|
| 总户数 | 数字输入 | 否 | 配合"户"单位标识 |
|
||||||
|
| 小区坐标 | 经纬度文本输入 | 否 | 格式:纬度,经度,旁有坐标编辑入口和地图定位按钮 |
|
||||||
|
|
||||||
|
- [ ] 必填字段未填写时,点击"确定"弹出错误提示并定位到未填字段
|
||||||
|
- [ ] 小区名称、小区地址字段灰底只读,不可编辑
|
||||||
|
|
||||||
|
#### 学校信息区块
|
||||||
|
|
||||||
|
- [ ] 支持关联多个对口学校,每条记录提供删除操作
|
||||||
|
- [ ] 提供"+ 添加"按钮,点击打开学校选择弹窗
|
||||||
|
- [ ] 区块说明文字:"删除学校,所有房源下关联的该关联学校将会被删除"
|
||||||
|
|
||||||
|
#### 其他信息区块
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 小区总建筑面积 | 数字输入 | 否 | 单位:m² |
|
||||||
|
| 小区占地面积 | 数字输入 | 否 | 单位:m² |
|
||||||
|
| 容积率 | 数字输入 | 否 | 如:1.7 |
|
||||||
|
| 绿化率 | 数字输入 | 否 | 如:38(%)|
|
||||||
|
| 开发商 | 文本输入 | 否 | 如:单位自建 |
|
||||||
|
| 物业公司 | 文本输入 | 否 | 如:业主自管 |
|
||||||
|
| 物业费 | 数字输入 | 否 | 单位:元/m²/月,如:1.20 |
|
||||||
|
| 物业电话 | 文本输入 | 否 | |
|
||||||
|
| 车位数(总数) | 数字输入 | 否 | 配合"个"单位 |
|
||||||
|
| 车位数(地下) | 数字输入 | 否 | 配合"个"单位 |
|
||||||
|
| 停车位配比 | 文本输入 | 否 | 如:100:63 |
|
||||||
|
| 供水类型 | 单选 | 否 | 民水 / 商水 |
|
||||||
|
| 供电类型 | 单选 | 否 | 民电 / 商电 |
|
||||||
|
| 统一供暖 | 单选 | 否 | 有 / 无 |
|
||||||
|
| 有无燃气 | 单选 | 否 | 有 / 无 |
|
||||||
|
| 备注 | 文本区域 | 否 | 多行文本 |
|
||||||
|
|
||||||
|
- [ ] 页面底部固定显示"确定"(橙色主按钮)和"取消"按钮
|
||||||
|
- [ ] 点击"取消"返回楼盘详情页,不保存
|
||||||
|
- [ ] 保存成功后返回楼盘信息 Tab,信息即时刷新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 4:运营人员管理楼栋和单元
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在楼盘详情的楼栋管理 Tab 中维护楼栋(单元)列表,**So that** 为房源录入提供准确的楼栋结构参考,减少"无法关联结构"的房源数量。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 楼栋管理 Tab 以列表形式展示该楼盘下所有单元,列字段包含:单元名、楼盘类型(标准/非标)、物业类型、竣工年限、总层数、土地使用年限、电梯(有/无)、关联学校
|
||||||
|
- [ ] 每行操作列提供"编辑"和"结构管理"两个操作链接
|
||||||
|
- [ ] 顶部支持按"单元"关键词搜索,点击"搜索"执行
|
||||||
|
- [ ] 批量操作:批量设置单元信息、合并单元、移动单元
|
||||||
|
- [ ] 提供"申请新增"入口(找不到楼栋时引导用户发起新增申请)
|
||||||
|
- [ ] 页面顶部提示文字:本楼盘还有N个非标准结构,提供"查看明细"跳转链接
|
||||||
|
- [ ] 列表分页(20条/页,支持翻页和跳页),底部显示总条数
|
||||||
|
- [ ] 单元名称为蓝色可点击链接,点击进入该单元的结构管理视图
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 5:运营人员管理结构(楼层与房号)
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在结构管理 Tab 中查看并维护每个单元下的楼层和房号信息,**So that** 构建准确的"楼盘-楼栋-单元-楼层-房号"五级数据结构,支撑房源的精准定位。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 结构管理 Tab 左侧为单元列表(支持多选),右侧展示选中单元的楼层-房号矩阵
|
||||||
|
- [ ] 左侧单元列表按单元名称列出,支持滚动,选中单元高亮显示(橙色)
|
||||||
|
- [ ] 右侧矩阵:行为楼层名(实际层),列为房号;矩阵单元格展示具体房号(附标准/非标标签)
|
||||||
|
- [ ] 矩阵顶部提供"批量编辑房号"和"合并房号"操作按钮
|
||||||
|
- [ ] 顶部显示"已选N条"计数,以及找不到房号时引导"申请新增"链接
|
||||||
|
- [ ] 右上角提示本楼盘还有N个房号无法关联结构,提供"查看明细"入口
|
||||||
|
- [ ] 每个房号旁显示"标准"标签(表示已匹配标准结构)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 6:运营人员管理楼盘照片
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在楼盘照片 Tab 中上传和管理楼盘的图片资源(楼盘图片、户型图、VR),**So that** 为经纪人展示楼盘和为买客提供参考提供丰富的视觉素材。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
|
||||||
|
#### 照片分类 Tab
|
||||||
|
|
||||||
|
- [ ] 支持三类照片 Tab:**楼盘图片(N)** / **户型图(N)** / **楼盘VR(N)**,括号内显示该类照片数量
|
||||||
|
|
||||||
|
#### 户型图管理
|
||||||
|
|
||||||
|
- [ ] 户型图支持按户型过滤:全部 / 1室 / 2室 / 3室 / 4室 / 5室及以上
|
||||||
|
- [ ] 支持按朝向过滤:全部 / 东 / 南 / 西 / 北 / 南北 / 东南 / 西北 / 东北 / 东西
|
||||||
|
- [ ] 户型图内部分子 Tab:推荐户型图(N)/ **标准户型图(N)**(当前激活)/ VR户型图(N)/ 本地上传(N)
|
||||||
|
- [ ] 系统提示:"推荐户型图和标准户型图仅支持查看"(标准库图片不可编辑/删除)
|
||||||
|
- [ ] 户型图以瀑布流/网格方式展示,每张图片底部标注户型描述(如"1室2厅1卫")
|
||||||
|
- [ ] 支持分页(30条/页),显示总条数
|
||||||
|
|
||||||
|
#### 楼盘图片管理
|
||||||
|
|
||||||
|
- [ ] 支持上传楼盘实景图片,支持批量上传
|
||||||
|
- [ ] 支持图片分类管理
|
||||||
|
|
||||||
|
#### 楼盘VR管理
|
||||||
|
|
||||||
|
- [ ] 支持上传/关联VR全景资源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 7:经纪人查看楼盘价格走势
|
||||||
|
|
||||||
|
**As** Agent(经纪人),**I want** 在楼盘详情页查看该楼盘的挂牌价走势和历史成交数据,**So that** 在带看时能为客户提供客观的市场行情参考,增强议价信心。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
|
||||||
|
#### 数据维度切换
|
||||||
|
|
||||||
|
- [ ] 价格走势 Tab 顶部提供两个子 Tab:**司内数据** / **市场数据**
|
||||||
|
- [ ] 顶部注明"以下数据按照T+1更新,市场&网签数据仅供参考"
|
||||||
|
|
||||||
|
#### 司内数据视图
|
||||||
|
|
||||||
|
- [ ] 展示两个摘要指标:本周小区挂牌均价(元/m²)、近一年小区成交均价
|
||||||
|
- [ ] **挂牌量分布区块**:
|
||||||
|
- 以户型为维度展示分布(如"3室挂牌分布情况")
|
||||||
|
- 环形图展示该户型挂牌套数占总挂牌比例
|
||||||
|
- 标注:3室挂牌套数 / 小区挂牌套数(蓝色可点击数字,跳转房源列表)
|
||||||
|
- 展示该户型挂牌价格分布:大多数业主的选择(中间价区间)/ 最低价 / 最高价(万)
|
||||||
|
- [ ] **成交分布情况区块**:展示户型维度成交分布,无数据时显示空状态"暂无数据"
|
||||||
|
- [ ] **挂牌均价趋势折线图**:
|
||||||
|
- 支持按"按周"/ "按月"切换时间粒度(按钮组切换)
|
||||||
|
- X轴为时间,Y轴为价格(万/m²)
|
||||||
|
- 双折线:本小区(橙红色实线)/ 本商圈(蓝色实线)
|
||||||
|
- 图例位于左上角,鼠标悬浮显示具体数值 Tooltip
|
||||||
|
- [ ] **成交均价趋势折线图**:
|
||||||
|
- X轴为时间(月份),Y轴为价格
|
||||||
|
- 单折线:本小区
|
||||||
|
- 无数据时图表显示空状态
|
||||||
|
- [ ] **本小区成交数据明细(近一年)**:
|
||||||
|
- 数据免责说明:"数据实时更新,公司设置了我售房源展示保护规则,仅列出展示权限内的成交记录"
|
||||||
|
- 表格列:房源编号 / 挂牌价格(万)/ 价差(万)/ 成交价格(万)/ 成交单价(元/m²)/ 成交周期(天)/ 户型 / 面积(m²)/ 楼层 / 朝向 / 装修 / 挂日期 / 成交日期
|
||||||
|
- 无数据时展示空状态"暂无数据"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 8:经纪人查看楼盘周边配套
|
||||||
|
|
||||||
|
**As** Agent(经纪人),**I want** 在楼盘详情页查看该楼盘周边的交通/教育/医疗/购物/生活/娱乐配套,**So that** 在带客时快速回答客户关于生活便利性的问题,增强成交转化。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 周边配套 Tab 以地图为主体,楼盘位置以橙色标记点展示在地图上
|
||||||
|
- [ ] 右侧面板提供分类 Tab 过滤:**交通** / **教育** / **医疗** / **购物** / **生活** / **娱乐**
|
||||||
|
- [ ] 教育类下提供二级过滤:**幼儿园** / **小学** / **中学** / **大学**
|
||||||
|
- [ ] 右侧列表展示该分类下周边设施,每条记录包含:
|
||||||
|
- 设施图标 + 设施名称
|
||||||
|
- 线路/地址(灰色小字)
|
||||||
|
- 距楼盘直线距离(如"1227米")
|
||||||
|
- [ ] 地图上以彩色 Pin 标注对应类别的设施位置,与右侧列表联动
|
||||||
|
- [ ] 地图支持缩放和拖拽操作
|
||||||
|
- [ ] 周边数据由第三方地图 API 提供(接入规范另行定义)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 9:运营人员管理城区与商圈
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在区域管理模块中维护城区和商圈的二级区域体系,**So that** 为房源、楼盘、客源的区域筛选和统计提供规范的地理基础数据。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
|
||||||
|
#### 区域管理入口
|
||||||
|
|
||||||
|
- [ ] 楼盘管理页面顶部 Tab 导航:楼盘 / **区域管理** / 学校管理 / 应用标准数据
|
||||||
|
|
||||||
|
#### 城区管理
|
||||||
|
|
||||||
|
- [ ] 切换至"城区管理"子 Tab,展示城区列表,字段:城区名称 / 商圈数量 / 楼盘数量 / 坐标
|
||||||
|
- [ ] 支持按城区名称关键词搜索("查询"按钮触发,"重置"清空)
|
||||||
|
- [ ] 支持按有无坐标过滤:不限 / 有坐标 / 无坐标
|
||||||
|
- [ ] 批量操作:合并城区(勾选后激活"合并城区"按钮)
|
||||||
|
- [ ] 操作列:修改 / 设置坐标
|
||||||
|
- [ ] "新增城区"按钮(橙色,右上角)
|
||||||
|
- [ ] 商圈数量和楼盘数量为蓝色可点击数字,点击跳转查看关联数据
|
||||||
|
- [ ] 分页(20条/页),显示总条数
|
||||||
|
|
||||||
|
#### 商圈管理
|
||||||
|
|
||||||
|
- [ ] 切换至"商圈管理"子 Tab,展示商圈列表,字段:城区名称 / 商圈名称(含标准标签)/ 楼盘数量 / 坐标
|
||||||
|
- [ ] 支持按商圈名称关键词搜索
|
||||||
|
- [ ] 支持按城区过滤(多个城区单选展示,如:上海周边/徐汇/宝山等)
|
||||||
|
- [ ] 支持按有无坐标过滤:不限 / 有坐标 / 无坐标
|
||||||
|
- [ ] 批量操作:合并商圈 / 转移商圈
|
||||||
|
- [ ] 操作列:修改 / 查看关联关系 / 设置坐标
|
||||||
|
- [ ] "新增商圈"按钮(橙色,右上角)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 10:运营人员新增/编辑商圈
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 通过弹窗快速新增或修改商圈信息,**So that** 保持区域数据的及时更新,不需要跳转页面打断工作流。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 点击"修改"或"新增商圈"触发浮窗(Modal)
|
||||||
|
- [ ] 浮窗标题:修改商圈 / 新增商圈
|
||||||
|
- [ ] 字段:
|
||||||
|
- **所属城区**(下拉,必填):选择该商圈归属的城区
|
||||||
|
- **商圈名称**(文本输入,必填):商圈名称,如"南通"
|
||||||
|
- [ ] 必填字段未填时,点击"确认修改"弹出错误提示
|
||||||
|
- [ ] 确认后浮窗关闭,商圈列表即时刷新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 11:运营人员查看商圈关联关系
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 查看本地商圈与标准商圈之间的映射关系,并在需要时修改关联,**So that** 跨区域数据统计时能正确聚合同一商圈下不同分公司的数据。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 点击商圈列表操作列"查看关联关系",跳转至"查看关联情况"独立页面
|
||||||
|
- [ ] 页面顶部筛选区:标准区域(下拉,请选择)/ 本地区域(下拉,默认回填当前商圈所属城区+商圈)
|
||||||
|
- [ ] 点击"查询"触发搜索,"重置"清空条件
|
||||||
|
- [ ] 结果列表字段:标准城市 / 标准城区 / 标准商圈 / 关联本地商圈 / 本地商圈所属城区 / 操作("变更"链接)
|
||||||
|
- [ ] 支持批量修改(勾选后激活"批量修改"按钮)
|
||||||
|
- [ ] 分页(20条/页)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 12:运营人员管理学校信息
|
||||||
|
|
||||||
|
**As** 运营/数据管理员,**I want** 在学校管理模块中维护学校基础信息,并将学校与楼盘关联,**So that** 经纪人在房源录入和客户带看时能快速调用准确的学区数据,提升学区房的推荐效率。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
|
||||||
|
#### 学校列表
|
||||||
|
|
||||||
|
- [ ] 楼盘管理顶部 Tab 导航切换至"学校管理"
|
||||||
|
- [ ] 支持按学校名称关键词搜索("查询"按钮触发)
|
||||||
|
- [ ] 支持按城区过滤(单选城区标签:不限 / 宝山 / 崇明 / 奉贤等)
|
||||||
|
- [ ] 批量操作:"批量删除"按钮(勾选后激活)
|
||||||
|
- [ ] 新增操作:"+ 新增学校"橙色按钮
|
||||||
|
- [ ] 列表字段:学校名称 / 城区 / 学校地址 / 类型(幼儿园/小学/初中/高中/九年制/九年一贯制等)/ 级别(普通/重点/区重点等)/ 性质(公立/私立)/ 操作(编辑/删除)
|
||||||
|
- [ ] 分页(20条/页),显示总条数(如"共1503条"),支持跳页
|
||||||
|
|
||||||
|
#### 编辑/新增学校(浮窗)
|
||||||
|
|
||||||
|
- [ ] 点击"编辑"或"新增学校"触发浮窗(Modal),标题:编辑学校 / 新增学校
|
||||||
|
- [ ] 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 学校名称 | 文本输入 | 是 | 红色*标注,输入框提示"学校名称" |
|
||||||
|
| 城区 | 下拉 | 是 | 红色*标注,选择所属行政区 |
|
||||||
|
| 地址 | 文本输入 | 否 | 学校具体地址,如"郭守敬路111号" |
|
||||||
|
| 学校类型 | 下拉 | 否 | 幼儿园 / 小学 / 初中 / 高中 / 九年制 / 九年一贯制 / 大学等 |
|
||||||
|
| 办学性质 | 下拉 | 否 | 公立 / 私立 |
|
||||||
|
| 级别 | 下拉 | 否 | 普通 / 重点 / 区重点 等 |
|
||||||
|
|
||||||
|
- [ ] 必填字段(学校名称、城区)未填时,点击"确定"弹出错误提示并定位到对应字段
|
||||||
|
- [ ] 点击"取消"关闭浮窗,不保存
|
||||||
|
- [ ] 保存成功后浮窗关闭,列表即时刷新,新增/修改的学校显示在列表中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 功能详细说明
|
||||||
|
|
||||||
|
### 5.1 楼盘列表
|
||||||
|
|
||||||
|
#### 5.1.1 页面结构
|
||||||
|
|
||||||
|
楼盘管理页面为系统管理后台的核心数据管理页面,整体布局如下:
|
||||||
|
|
||||||
|
**顶部 Tab 导航**(模块级):
|
||||||
|
- 楼盘(当前)
|
||||||
|
- 区域管理
|
||||||
|
- 学校管理
|
||||||
|
- 应用标准数据(本期不做)
|
||||||
|
|
||||||
|
**数据完整度统计面板**(顶部横向展示):
|
||||||
|
|
||||||
|
| 指标 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 楼盘关联率 | 有房源关联的楼盘占比 |
|
||||||
|
| 楼栋及单元完整率 | 已完善楼栋/单元信息的楼盘占比 |
|
||||||
|
| 房号匹配率 | 房源已匹配到具体房号的占比 |
|
||||||
|
| 处置率 | 已处置异常数据的占比 |
|
||||||
|
| 入住人结构数据 | 有入住人信息的结构数量 |
|
||||||
|
| 有效结构数量 | 系统中有效结构总量 |
|
||||||
|
| 房源对标 | 房源与标准结构匹配度 |
|
||||||
|
|
||||||
|
提供"重新计算"按钮手动刷新统计数据。
|
||||||
|
|
||||||
|
#### 5.1.2 搜索与筛选
|
||||||
|
|
||||||
|
**关键词搜索**:
|
||||||
|
- 搜索范围:楼盘名称 / 别名 / 供货商 / 详细地址
|
||||||
|
- 点击"查询"执行,"清除"重置
|
||||||
|
|
||||||
|
**维度筛选**(水平横排,支持多维组合):
|
||||||
|
|
||||||
|
| 筛选维度 | 选项示例 |
|
||||||
|
|----------|----------|
|
||||||
|
| 区域 | 不限 / 静安 / 闵行 / 普陀 / 松江 / 长宁 等行政区 |
|
||||||
|
| 用途 | 不限 / 住宅 / 别墅 / 商住 / 商业 / 写字楼 / 其他 |
|
||||||
|
| 固定情况 | 下拉选择 |
|
||||||
|
| 完善情况 | 下拉选择 |
|
||||||
|
| 楼盘类型 | 下拉选择 |
|
||||||
|
| 楼标号小区非标结构情况 | 下拉选择 |
|
||||||
|
| 有无房源 | 下拉选择 |
|
||||||
|
| 楼栋类型 | 下拉选择 |
|
||||||
|
| 权属关系 | 下拉选择 |
|
||||||
|
| 有无坐标 | 下拉选择 |
|
||||||
|
|
||||||
|
#### 5.1.3 列表字段说明
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 楼盘名称 | 蓝色可点击链接,跳转楼盘详情;行内附"信息"/"标准楼盘"/"标准楼栋"/"标准房号"等快捷标签 |
|
||||||
|
| 楼盘类型 | 住宅/别墅/商住/商业等 |
|
||||||
|
| 详细地址 | 楼盘完整地址 |
|
||||||
|
| 城区商圈 | 所属城区-商圈 |
|
||||||
|
| 当月挂牌均价(元/m²) | 本月该楼盘挂牌房源的平均单价,支持排序 |
|
||||||
|
| 楼栋数 | 该楼盘下已录入的楼栋总数,数字可点击 |
|
||||||
|
| 产品数 | 房源/户型产品数量 |
|
||||||
|
| 房源数 | 格式:出售N/出租N/共N,蓝色数字可点击跳转房源列表 |
|
||||||
|
| 操作 | 编辑 / 删除 |
|
||||||
|
|
||||||
|
#### 5.1.4 批量操作
|
||||||
|
|
||||||
|
| 操作 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 批量新增楼栋 | 为勾选楼盘批量新增楼栋 |
|
||||||
|
| 批改区域商圈 | 批量修改选中楼盘的所属区域/商圈 |
|
||||||
|
| 删除 | 批量删除(需二次确认) |
|
||||||
|
| 合并楼盘 | 将多个楼盘合并为一个标准楼盘 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 楼盘详情
|
||||||
|
|
||||||
|
#### 5.2.1 详情页顶部区域
|
||||||
|
|
||||||
|
**楼盘标题**:展示楼盘的完整名称(主名称 + 括号内别名列表),多个别名以顿号分隔。
|
||||||
|
|
||||||
|
**权限标签**(4类锁定标志,锁状图标):
|
||||||
|
|
||||||
|
| 标签 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| 楼栋 🔒 | 楼栋信息已锁定,不可随意修改 |
|
||||||
|
| 房号 🔒 | 房号信息已锁定 |
|
||||||
|
| 信息 🔒 | 楼盘基本信息已锁定 |
|
||||||
|
| 标准房号 🔒 | 已关联标准房号,不可随意变更 |
|
||||||
|
|
||||||
|
**"解锁楼盘"按钮**(橙色,右上角,受权限控制)
|
||||||
|
|
||||||
|
**Tab 导航**(楼盘详情内部 Tab):
|
||||||
|
|
||||||
|
| Tab | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| 楼盘信息 | 楼盘基础数据(基本信息/对口学校/其他信息) |
|
||||||
|
| 楼栋管理 | 楼栋/单元列表管理 |
|
||||||
|
| 结构管理 | 楼层-房号矩阵管理 |
|
||||||
|
| 楼盘照片 | 楼盘图片/户型图/VR管理 |
|
||||||
|
| 楼盘附件 | 楼盘相关文件附件 |
|
||||||
|
| 周边配套 | 地图+周边设施信息 |
|
||||||
|
| 楼盘价格走势 | 挂牌价/成交价走势图表 |
|
||||||
|
| 销控盘 | 新房/销控相关(本期不展开) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 楼栋管理
|
||||||
|
|
||||||
|
楼栋管理采用列表视图,以"单元"为基本管理单元(对于别墅类楼盘,每个独立门牌号视为一个单元)。
|
||||||
|
|
||||||
|
**关键设计决策**:
|
||||||
|
- 楼栋结构的最小粒度为"单元",单元下才挂楼层和房号
|
||||||
|
- 标准单元有"标准"标签,非标结构另行标记,支持通过"申请新增"发起数据标准化申请
|
||||||
|
- 批量操作(设置单元信息/合并/移动)支持跨楼盘的单元管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 结构管理
|
||||||
|
|
||||||
|
结构管理提供"左侧单元列表 + 右侧楼层-房号矩阵"的双栏布局:
|
||||||
|
|
||||||
|
- **左侧**:当前楼盘所有单元列表,支持多选;选中单元以橙色高亮,矩阵区同步更新
|
||||||
|
- **右侧矩阵**:行为楼层名(显示实际层数,如"1(1层)"),列为房号,矩阵单元格显示具体房号及标准/非标标签
|
||||||
|
|
||||||
|
**设计原则**:矩阵布局让数据管理员能一眼看清每层每号的覆盖情况,快速定位缺失房号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 楼盘照片
|
||||||
|
|
||||||
|
照片管理分三类 Tab,不同类别的照片有不同的管理逻辑:
|
||||||
|
|
||||||
|
| 类别 | 上传权限 | 管理方式 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 楼盘图片 | 运营人员可上传 | 自由上传,分类管理 |
|
||||||
|
| 户型图 | 标准库只读,本地上传可维护 | 标准户型图不可编辑;推荐图/本地上传可管理 |
|
||||||
|
| 楼盘VR | 运营人员可上传 | 上传 VR 资源文件 |
|
||||||
|
|
||||||
|
户型图的子 Tab 分类机制:
|
||||||
|
- **推荐户型图**:系统推荐的标准图,只读查看
|
||||||
|
- **标准户型图**:标准数据库中的户型图,只读查看
|
||||||
|
- **VR户型图**:VR 格式的户型图
|
||||||
|
- **本地上传**:公司自行上传的户型图,可编辑
|
||||||
|
|
||||||
|
户型图支持按户型(室数)和朝向双维度过滤,方便快速定位特定类型的户型图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 楼盘价格走势
|
||||||
|
|
||||||
|
价格走势功能提供楼盘级别的市场行情可视化,帮助经纪人建立数据支撑下的市场认知。
|
||||||
|
|
||||||
|
**数据来源说明**:
|
||||||
|
- **司内数据**:来自本公司系统内房源挂牌和成交记录,T+1 更新,数据准确但可能样本量有限
|
||||||
|
- **市场数据**:来自市场/网签数据,T+1 更新,仅供参考
|
||||||
|
|
||||||
|
**核心可视化组件**:
|
||||||
|
|
||||||
|
1. **挂牌量分布图(环形图)**:直观展示各户型在总挂牌量中的占比,配合价格区间(最低/大多数业主选择/最高)为经纪人提供定价参考
|
||||||
|
2. **成交分布图**:户型维度的历史成交情况
|
||||||
|
3. **挂牌均价趋势折线图**:支持按周/按月切换,双折线(小区 vs 商圈)对比,帮助经纪人判断本楼盘相对商圈的价格偏离度
|
||||||
|
4. **成交均价趋势折线图**:月度维度的成交均价历史走势
|
||||||
|
5. **成交明细表格**:近一年成交记录,字段完整,支持经纪人做具体的价格比对分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.7 周边配套
|
||||||
|
|
||||||
|
周边配套采用"地图主视图 + 右侧分类列表"的双栏布局,数据由第三方地图 API 提供。
|
||||||
|
|
||||||
|
**分类体系**:
|
||||||
|
|
||||||
|
| 一级分类 | 二级分类(示例) |
|
||||||
|
|---------|----------------|
|
||||||
|
| 交通 | 地铁站/公交站/高速出入口等 |
|
||||||
|
| 教育 | 幼儿园 / 小学 / 中学 / 大学 |
|
||||||
|
| 医疗 | 医院/诊所/药店等 |
|
||||||
|
| 购物 | 超市/商场/菜市场等 |
|
||||||
|
| 生活 | 银行/邮局/政务服务等 |
|
||||||
|
| 娱乐 | 公园/影院/健身房等 |
|
||||||
|
|
||||||
|
每条设施记录展示:名称 + 地址/线路 + 距楼盘直线距离(米)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.8 区域管理
|
||||||
|
|
||||||
|
区域管理分为"城区管理"和"商圈管理"两个子模块,共同构建城区-商圈两级区域数据体系。
|
||||||
|
|
||||||
|
#### 5.8.1 城区管理
|
||||||
|
|
||||||
|
城区为区域体系的第一级,对应行政区划(如:闵行/长宁/嘉定等)。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- 列表展示(城区名称/商圈数量/楼盘数量/坐标)
|
||||||
|
- 支持合并城区(处理历史数据中的同一区域多名称问题)
|
||||||
|
- 支持修改城区名称
|
||||||
|
- 支持设置城区坐标(经纬度,用于地图展示)
|
||||||
|
|
||||||
|
#### 5.8.2 商圈管理
|
||||||
|
|
||||||
|
商圈为区域体系的第二级,归属于特定城区(如:嘉定-江桥新城)。
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- 列表展示(城区名称/商圈名称/楼盘数量/坐标)
|
||||||
|
- 新增/修改商圈(浮窗操作,字段:所属城区+商圈名称)
|
||||||
|
- 合并商圈(多个历史商圈名称合并为一个标准商圈)
|
||||||
|
- 转移商圈(将商圈从一个城区移至另一个城区)
|
||||||
|
- 设置坐标
|
||||||
|
- 查看关联关系
|
||||||
|
|
||||||
|
#### 5.8.3 商圈关联关系
|
||||||
|
|
||||||
|
商圈关联关系用于处理本地区域数据与全国标准区域数据的映射,支持跨城市分公司的数据统一。
|
||||||
|
|
||||||
|
**页面字段**:标准城市 / 标准城区 / 标准商圈 / 关联本地商圈 / 本地商圈所属城区 / 操作(变更)
|
||||||
|
|
||||||
|
**使用场景**:当系统引入国家/行业标准区域体系时,需要将历史本地商圈数据映射至标准商圈,此页面提供查看和变更能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.9 学校管理
|
||||||
|
|
||||||
|
学校管理维护可供楼盘和房源关联的学校数据库,是学区房推荐的基础数据支撑。
|
||||||
|
|
||||||
|
#### 5.9.1 学校列表
|
||||||
|
|
||||||
|
支持按名称搜索 + 城区过滤,展示字段:学校名称/城区/学校地址/类型/级别/性质。
|
||||||
|
|
||||||
|
**学校类型枚举**:幼儿园 / 小学 / 初中 / 高中 / 九年制 / 九年一贯制 / 大学
|
||||||
|
|
||||||
|
**学校级别枚举**:普通 / 重点 / 区重点
|
||||||
|
|
||||||
|
**办学性质枚举**:公立 / 私立
|
||||||
|
|
||||||
|
#### 5.9.2 新增/编辑学校(浮窗)
|
||||||
|
|
||||||
|
浮窗操作,字段简洁:学校名称(必填)/ 城区(必填)/ 地址 / 学校类型 / 办学性质 / 级别。
|
||||||
|
|
||||||
|
必填项校验,确认后列表即时刷新,无需跳转页面。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 技术考量
|
||||||
|
|
||||||
|
### 6.1 依赖项
|
||||||
|
|
||||||
|
| 系统/模块 | 依赖原因 | 时间线风险 |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| 地图服务 API | 周边配套数据来源、楼盘坐标定位功能 | 中(需确定采购哪家地图供应商) |
|
||||||
|
| 第三方价格数据 | 楼盘价格走势-市场数据 Tab | 中(数据接口规范需另行对接) |
|
||||||
|
| 房源管理模块 | 房源与楼盘的关联关系 | 低(已有设计) |
|
||||||
|
| 权限管理模块 | 楼盘锁定/解锁权限、数据编辑权限 | 低(权限模块统一管理) |
|
||||||
|
|
||||||
|
### 6.2 已知风险
|
||||||
|
|
||||||
|
| 风险 | 可能性 | 影响 | 缓解措施 |
|
||||||
|
|------|-------|------|---------|
|
||||||
|
| 历史楼盘数据清洗工作量大 | 高 | 高 | 上线前做数据迁移专项,优先处理有房源关联的楼盘 |
|
||||||
|
| 地图 API 数据延迟/不准确 | 中 | 低 | 周边配套数据仅供参考,界面明确标注数据来源 |
|
||||||
|
| 楼栋结构标准化周期长 | 高 | 中 | 分阶段推进,先保障主要楼盘,长尾楼盘后续持续补充 |
|
||||||
|
| 标准区域体系与本地区域冲突 | 中 | 中 | 提供关联关系映射功能,不强制替换本地区域体系 |
|
||||||
|
|
||||||
|
### 6.3 待确认问题(开发前必须解决)
|
||||||
|
|
||||||
|
- [ ] **坐标系标准**:楼盘坐标采用 WGS84 还是 GCJ-02(国测局坐标)?— Owner: 技术负责人 — 截止: 开发启动前
|
||||||
|
- [ ] **地图 API 选型**:周边配套数据采用高德/百度/腾讯地图哪个 API?— Owner: 产品/采购 — 截止: 开发启动前
|
||||||
|
- [ ] **楼盘锁定权限粒度**:楼盘/房号/楼栋/信息四类锁各自对应哪些角色可以编辑/解锁?— Owner: 产品经理 + 客户方确认 — 截止: 开发启动前
|
||||||
|
- [ ] **历史数据迁移策略**:现有楼盘数据如何迁移到新系统?是否需要数据清洗脚本?— Owner: 技术负责人 — 截止: 开发启动前
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 上线计划
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 受众 | 成功标准 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 内部 Alpha | TBD | 产品+技术+运营团队 | 核心流程无 P0 Bug,数据增删改查正常 |
|
||||||
|
| 运营灰度 | TBD | 数据管理员(3-5人) | 楼盘/楼栋/区域/学校 CRUD 功能可用,无数据丢失 |
|
||||||
|
| GA | TBD | 全员开放 | 楼盘完整度指标提升,经纪人可正常查询楼盘详情和价格走势 |
|
||||||
|
|
||||||
|
**回滚标准**:楼盘数据查询错误率 > 1% 或核心写操作失败率 > 0.5%,立即回滚并告警。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 附录
|
||||||
|
|
||||||
|
### 8.1 截图参考索引
|
||||||
|
|
||||||
|
| 截图文件 | 路径 | 对应章节 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `楼盘管理.png` | `Project/fonrey/screenshots/楼盘管理/楼盘管理.png` | 5.1 楼盘列表 |
|
||||||
|
| `楼盘信息.png` | `Project/fonrey/screenshots/楼盘管理/楼盘信息.png` | 5.2.1 楼盘信息 Tab(查看态) |
|
||||||
|
| `编辑楼盘信息.png` | `Project/fonrey/screenshots/楼盘管理/编辑楼盘信息.png` | Story 3 / 5.2.1(编辑态) |
|
||||||
|
| `楼栋管理.png` | `Project/fonrey/screenshots/楼盘管理/楼栋管理.png` | 5.3 楼栋管理 |
|
||||||
|
| `结构管理.png` | `Project/fonrey/screenshots/楼盘管理/结构管理.png` | 5.4 结构管理 |
|
||||||
|
| `楼盘照片.png` | `Project/fonrey/screenshots/楼盘管理/楼盘照片.png` | 5.5 楼盘照片(户型图 Tab) |
|
||||||
|
| `楼盘价格走势.png` | `Project/fonrey/screenshots/楼盘管理/楼盘价格走势.png` | 5.6 楼盘价格走势(司内数据) |
|
||||||
|
| `周边配套.png` | `Project/fonrey/screenshots/楼盘管理/周边配套.png` | 5.7 周边配套(教育-幼儿园) |
|
||||||
|
| `区域管理.png` | `Project/fonrey/screenshots/楼盘管理/区域管理.png` | 5.8.1 城区管理列表 |
|
||||||
|
| `编辑商圈.png` | `Project/fonrey/screenshots/楼盘管理/编辑商圈.png` | 5.8.2 商圈管理-编辑浮窗 |
|
||||||
|
| `查看关联.png` | `Project/fonrey/screenshots/楼盘管理/查看关联.png` | 5.8.3 商圈关联关系页面 |
|
||||||
|
| `学校管理.png` | `Project/fonrey/screenshots/楼盘管理/学校管理.png` | 5.9.1 学校列表 |
|
||||||
|
| `编辑学校.png` | `Project/fonrey/screenshots/楼盘管理/编辑学校.png` | 5.9.2 新增/编辑学校浮窗 |
|
||||||
|
|
||||||
|
### 8.2 数据枚举汇总
|
||||||
|
|
||||||
|
**楼盘类型(物业类型)**:住宅 / 别墅 / 商住 / 商业 / 写字楼 / 其他
|
||||||
|
|
||||||
|
**建筑类型**:板楼 / 塔楼 / 板塔结合
|
||||||
|
|
||||||
|
**楼栋结构**:单元-房号 / 其他
|
||||||
|
|
||||||
|
**土地使用年限**:40年 / 50年 / 70年 / 永久产权
|
||||||
|
|
||||||
|
**权属类别**:商品房住宅 / 房改房 / 集资房 / 经济活用房
|
||||||
|
|
||||||
|
**学校类型**:幼儿园 / 小学 / 初中 / 高中 / 九年制 / 九年一贯制 / 大学
|
||||||
|
|
||||||
|
**学校级别**:普通 / 重点 / 区重点
|
||||||
|
|
||||||
|
**办学性质**:公立 / 私立
|
||||||
|
|
||||||
|
**周边配套一级分类**:交通 / 教育 / 医疗 / 购物 / 生活 / 娱乐
|
||||||
|
|
||||||
|
**户型图类型(子Tab)**:推荐户型图 / 标准户型图 / VR户型图 / 本地上传
|
||||||
340
Project/fonrey/PRD/权限管理/PERMISSION_SEED_MVP_BATCH1.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# PermissionDef 种子数据规格 — MVP 批次 1
|
||||||
|
|
||||||
|
> **数据来源**:竞品权限参考文档 + 各模块 PRD
|
||||||
|
> **实施方式**:Django Data Migration(RunPython)写入 `public` schema
|
||||||
|
> **字段说明**:`default_value` 用于租户首次配置前的兜底值;`scope` 类型取值 `none/self/dept/all`(对应无/本人/本部/全部);`integer` 类型 `-1` = 不限制,`0` = 不允许
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 房源模块(二手租赁)(66 条)
|
||||||
|
|
||||||
|
### 房源基础
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| ------------------------------------ | ------------ | ---------- | ------------- | ------------------------- | --- | --- | --- | --- |
|
||||||
|
| `property.listing.create` | 新增房源 | boolean | `false` | 是否可新增房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_scope` | 维护房源查看范围 | scope | `self` | 按维护人范围查看房源列表:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_public` | 公盘查看 | boolean | `false` | 是否可查看公盘房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_private` | 私盘查看 | boolean | `false` | 是否可查看私盘房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.set_public` | 将房源改为公盘 | boolean | `false` | 是否可将房源属性改为公盘 | True | False | True | 1 |
|
||||||
|
| `property.listing.set_private` | 将房源改为私盘 | boolean | `false` | 是否可将房源属性改为私盘 | True | False | True | 1 |
|
||||||
|
| `property.listing.set_locked` | 将房源改为封盘 | boolean | `false` | 是否可将房源属性改为封盘 | True | False | True | 1 |
|
||||||
|
| `property.listing.set_special` | 将房源改为特盘 | boolean | `false` | 是否可将房源属性改为特盘 | True | False | True | 1 |
|
||||||
|
| `property.listing.delete` | 删除房源 | boolean | `false` | 是否可删除房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.restore` | 恢复已删除房源 | boolean | `false` | 是否可恢复已删除的房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.export` | 房源列表数据导出 | boolean | `false` | 是否可将房源列表数据导出 | True | False | True | 1 |
|
||||||
|
| `property.listing.edit_description` | 修改房屋介绍信息 | boolean | `false` | 是否可修改营销标题、核心卖点、户型介绍等介绍信息 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_deal` | 成交房源列表及价格信息 | boolean | `false` | 是否可查看成交房源列表及价格历史 | True | False | True | 1 |
|
||||||
|
| `property.listing.price_read` | 价格解读 | boolean | `false` | 是否可查看房源详情页的价格解读 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_history` | 查看房源挂牌历史 | boolean | `false` | 是否可查看房源挂牌历史记录 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_owner_others` | 查看同业主其他房源 | boolean | `false` | 是否可在房源详情页查看同业主的其他房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.set_protected` | 修改房源保护设置 | boolean | `false` | 是否可修改房源的保护期/保护房设置 | True | False | True | 1 |
|
||||||
|
| `property.listing.view_protected` | 查看保护期内房源 | boolean | `false` | 是否可查看保护期内(我租/我售/已售/已租)房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.change_keeper` | 修改相关方范围 | scope | `none` | 可修改哪个范围内房源的相关方:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.listing.merge_duplicate` | 重复房源合并 | boolean | `false` | 是否可合并重复房源 | True | False | True | 1 |
|
||||||
|
| `property.listing.status_sold` | 修改为我售/我租状态 | boolean | `false` | 是否可修改房源为我售/我租状态 | True | False | True | 1 |
|
||||||
|
| `property.listing.grade_set_a` | 将房源等级设为A | boolean | `false` | 是否可将挂牌中房源等级设为A(急迫) | True | False | True | 1 |
|
||||||
|
| `property.listing.grade_set_e` | 将房源等级设为E | boolean | `false` | 是否可将房源等级设为E(暂不关注) | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 业主/联系人与号码
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.contact.view_phone` | 查看业主/联系人号码 | boolean | `false` | 是否可查看房源业主/联系人真实号码 | True | False | True | 1 |
|
||||||
|
| `property.contact.view_phone_limit` | 每日查看号码次数上限 | integer | `0` | 每天可查看房源真实号码次数,0=不允许,-1=不限制 | True | False | True | 1 |
|
||||||
|
| `property.contact.add_contact` | 新增业主/联系人 | boolean | `false` | 是否可新增业主/联系人 | True | False | True | 1 |
|
||||||
|
| `property.contact.edit_core` | 修改业主核心信息 | scope | `none` | 可修改哪个范围的业主核心信息(电话/微信):无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.contact.edit_basic` | 修改业主非核心信息 | scope | `self` | 可修改哪个范围的业主基本信息(姓名/备注):无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.contact.delete_contact` | 删除业主/联系人 | boolean | `false` | 是否可删除业主/联系人 | True | False | True | 1 |
|
||||||
|
| `property.contact.view_cert` | 查看产证信息 | boolean | `false` | 是否可查看房源详情页产证信息 | True | False | True | 1 |
|
||||||
|
| `property.contact.view_operation_log` | 查看业主联系人操作日志 | boolean | `false` | 是否可查看业主/联系人的新增、修改、删除等记录 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源地址
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.address.view_detail` | 查看楼栋/单元/楼层/房号 | boolean | `false` | 是否可查看房源真实地址详情 | True | False | True | 1 |
|
||||||
|
| `property.address.view_limit` | 每日查看地址次数上限 | integer | `0` | 每天可查看房源真实地址总次数,0=不允许,-1=不限制 | True | False | True | 1 |
|
||||||
|
| `property.address.edit` | 修改楼栋/单元/楼层/房号 | boolean | `false` | 是否可修改房源地址信息 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源钥匙
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.key.create` | 新增钥匙 | boolean | `false` | 是否可新增钥匙 | True | False | True | 1 |
|
||||||
|
| `property.key.edit` | 修改钥匙 | scope | `none` | 按钥匙方范围可修改钥匙:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.return` | 退还钥匙 | scope | `none` | 按钥匙方范围控制是否可退还钥匙:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.view_password` | 查看钥匙密码 | scope | `none` | 按钥匙方范围控制是否可查看密码:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.view_number` | 查看钥匙编号 | scope | `none` | 按钥匙方范围控制是否可查看钥匙编号:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.borrow` | 钥匙借出 | scope | `none` | 按钥匙保管部门范围借出钥匙:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.give_back` | 钥匙归还 | scope | `none` | 按钥匙保管部门范围归还钥匙:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.delete` | 删除钥匙 | scope | `none` | 按钥匙方范围控制是否可删除钥匙:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.key.export` | 钥匙列表数据导出 | boolean | `false` | 是否可导出钥匙数据 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源实勘
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.survey.create_photo` | 新增实勘图片 | boolean | `false` | 是否可新增实勘图片 | True | False | True | 1 |
|
||||||
|
| `property.survey.download_photo` | 下载图片 | boolean | `false` | 是否可下载实勘图片 | True | False | True | 1 |
|
||||||
|
| `property.survey.delete_photo` | 删除图片 | scope | `none` | 按图片上传人范围删除:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.survey.create` | 新增实勘 | boolean | `false` | 是否可新增实勘记录 | True | False | True | 1 |
|
||||||
|
| `property.survey.view` | 查看实勘 | boolean | `false` | 是否可查看实勘记录 | True | False | True | 1 |
|
||||||
|
| `property.survey.upload_video` | 上传视频 | boolean | `false` | 是否可上传房源视频 | True | False | True | 1 |
|
||||||
|
| `property.survey.download_video` | 下载视频 | boolean | `false` | 是否可下载房源视频 | True | False | True | 1 |
|
||||||
|
| `property.survey.play_video` | 播放视频 | boolean | `false` | 是否可播放房源视频 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源委托
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.mandate.create` | 新增委托 | boolean | `false` | 是否可新增房源委托 | True | False | True | 1 |
|
||||||
|
| `property.mandate.renew` | 续签/违约委托 | scope | `none` | 按委托方范围可续签或违约委托:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.mandate.view` | 委托列表查看 | scope | `none` | 按委托方范围查看委托列表:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.mandate.revoke` | 委托作废 | scope | `none` | 按委托方范围控制是否可作废委托:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.mandate.export` | 委托列表数据导出 | boolean | `false` | 是否可导出委托列表数据 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源跟进
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.follow.view_scope` | 查看房源跟进范围 | scope | `self` | 控制房源详情页的跟进查看范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.follow.hide` | 隐藏/开放跟进 | scope | `none` | 按跟进人范围隐藏/开放跟进:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.follow.view_hidden` | 查看隐藏跟进 | scope | `none` | 按跟进人范围查看被隐藏的跟进:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.follow.pin` | 置顶/取消置顶跟进 | scope | `none` | 按跟进人范围置顶或取消置顶跟进:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源附件
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.attachment.create` | 新增附件 | boolean | `false` | 是否可新增房源附件 | True | False | True | 1 |
|
||||||
|
| `property.attachment.view` | 查看附件 | scope | `none` | 按附件上传人范围查看:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.attachment.edit` | 修改附件 | scope | `none` | 按附件上传人范围修改:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `property.attachment.download` | 下载附件 | boolean | `false` | 是否可下载房源附件 | True | False | True | 1 |
|
||||||
|
| `property.attachment.delete` | 删除附件 | scope | `none` | 按附件上传人范围删除:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 房源带看
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `property.showing.view_scope` | 查看房源带看数据 | scope | `none` | 按数据权限范围查看房源带看记录:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 客源模块(36 条)
|
||||||
|
|
||||||
|
### 私客
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.private.create` | 新增私客 | boolean | `false` | 是否可新增私客 | True | False | True | 1 |
|
||||||
|
| `client.private.view` | 查看私客(非保护客) | scope | `self` | 按归属人范围查看非保护私客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.private.view_protected` | 查看私客(保护客) | scope | `self` | 按归属人范围查看保护私客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.private.edit` | 编辑私客(非保护客) | scope | `self` | 按归属人范围编辑非保护私客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.private.edit_protected` | 编辑私客(保护客) | scope | `self` | 按归属人范围编辑保护私客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.private.set_protected` | 设置/取消保护客 | scope | `self` | 设置/取消哪个范围的保护客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.private.to_public` | 私客转公客 | scope | `self` | 按归属人范围将私客转为公客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.private.export` | 私客列表导出 | boolean | `false` | 是否支持导出私客列表 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 公客
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.public.view` | 公客查看范围 | scope | `none` | 控制公客查看范围:无/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.public.to_private` | 公客转私客 | boolean | `false` | 是否可将可见范围内的公客转为私客 | True | False | True | 1 |
|
||||||
|
| `client.public.edit` | 编辑公客 | boolean | `false` | 是否可编辑公客信息 | True | False | True | 1 |
|
||||||
|
| `client.public.change_status` | 改公客状态 | boolean | `false` | 是否可修改公客状态 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 成交客
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.deal.view` | 查看成交客(私客类型) | scope | `self` | 按归属人范围查看归属人为个人的成交客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.deal.view_public` | 查看成交客(公客类型) | scope | `none` | 按归属人范围查看归属人为共享账号的成交客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.deal.re_transaction` | 成交客再次租/购 | boolean | `false` | 是否可对可见范围内成交客操作再次租/购 | True | False | True | 1 |
|
||||||
|
| `client.deal.export` | 导出成交客列表 | boolean | `false` | 是否可导出成交客列表 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 联系人号码
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.contact.view_phone_private` | 查看私客/成交客号码 | scope | `none` | 控制查看非保护私客及成交客的号码范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.contact.view_phone_protected` | 查看保护客号码 | scope | `none` | 控制查看保护私客的号码范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.contact.view_phone_public` | 查看公客号码 | scope | `none` | 控制查看公客号码范围:无/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.contact.view_phone_limit` | 每日查看联系人号码次数上限 | integer | `0` | 每天可查看客源联系人真实号码次数,0=不允许,-1=不限制 | True | False | True | 1 |
|
||||||
|
| `client.contact.edit_contact` | 编辑私客/成交客联系人 | scope | `none` | 控制编辑非保护私客及成交客联系人信息范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.contact.edit_phone` | 编辑私客/成交客联系人号码 | scope | `none` | 控制编辑非保护私客及成交客联系人号码范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 客源管理
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.mgmt.delete` | 删除客源 | boolean | `false` | 是否可删除客源及查看已删除客源 | True | False | True | 1 |
|
||||||
|
| `client.mgmt.to_deal` | 手动客源转为成交客 | boolean | `false` | 是否可手动将客源标记为成交客 | True | False | True | 1 |
|
||||||
|
| `client.mgmt.change_staff` | 单个客源修改相关员工 | scope | `none` | 可修改哪个范围内客源的相关方:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.mgmt.batch_change_staff` | 批量客源修改相关员工 | scope | `none` | 批量修改哪个范围内客源的相关方:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.mgmt.view_operation_log` | 查看客户/联系人操作日志 | boolean | `false` | 是否可查看客户详情页手机号修改/删除、客户合并等记录 | True | False | True | 1 |
|
||||||
|
| `client.mgmt.merge_private` | 允许合并自己的私客 | boolean | `false` | 是否可合并归属人为本人的私客 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 带看/预约
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.showing.create` | 带看/预约新增 | boolean | `false` | 是否可新增带看/预约记录 | True | False | True | 1 |
|
||||||
|
| `client.showing.view` | 私客/成交客详情页带看单查看 | scope | `self` | 按带看人范围查看私客/成交客的带看单:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.showing.edit` | 带看/预约编辑、作废 | scope | `self` | 按带看人范围编辑或作废带看:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 资料客
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `client.archive.view` | 查看资料客 | scope | `self` | 按归属人范围查看资料客:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `client.archive.import` | 导入资料客 | boolean | `false` | 是否可导入资料客 | True | False | True | 1 |
|
||||||
|
| `client.archive.view_phone` | 查看资料客号码 | boolean | `false` | 是否可查看资料客号码 | True | False | True | 1 |
|
||||||
|
| `client.archive.delete` | 删除资料客 | boolean | `false` | 是否可删除资料客 | True | False | True | 1 |
|
||||||
|
| `client.archive.view_log` | 查看资料客操作日志 | scope | `self` | 查看哪个范围员工的资料客操作日志:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 首页模块(4 条)
|
||||||
|
|
||||||
|
### 首页看板
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `home.dashboard.view_version` | 查看首页版本 | boolean | `false` | 控制员工可查看的首页数据版本(置业顾问/店管/区管等) | True | False | True | 1 |
|
||||||
|
| `home.dashboard.personal_rank` | 个人排行榜权限 | scope | `self` | 控制个人排行榜可见数据范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `home.dashboard.dept_rank` | 部门排行榜权限 | scope | `none` | 控制部门排行榜可见数据范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `home.dashboard.manage_praise` | 管理点赞信息和屏蔽点赞 | boolean | `false` | 是否可删除首页点赞墙内容和禁止员工发布 | True | False | True | 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 楼盘/小区模块(27 条)
|
||||||
|
|
||||||
|
### 楼盘管理
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| -------------------------------------- | ------------ | ---------- | ------------- | ---------------------------- | --- | --- | --- | --- |
|
||||||
|
| `complex.view` | 楼盘管理查看 | boolean | `false` | 是否显示楼盘管理模块 | True | False | True | 1 |
|
||||||
|
| `complex.view_structure` | 楼盘结构查看 | boolean | `false` | 是否可查看楼栋-单元-房号层级结构 | True | False | True | 1 |
|
||||||
|
| `complex.create` | 新增楼盘 | boolean | `false` | 是否可新增或批量新增楼盘 | True | False | True | 1 |
|
||||||
|
| `complex.create_unit` | 新增楼栋/单元/房号 | boolean | `false` | 是否可新增楼栋、单元、房号数据 | True | False | True | 1 |
|
||||||
|
| `complex.edit` | 编辑楼盘 | boolean | `false` | 是否可编辑楼盘信息 | True | False | True | 1 |
|
||||||
|
| `complex.edit_unit` | 编辑楼栋/单元/房号 | boolean | `false` | 是否可编辑楼栋、单元、房号信息 | True | False | True | 1 |
|
||||||
|
| `complex.delete` | 删除楼盘 | boolean | `false` | 是否可删除楼盘 | True | False | True | 1 |
|
||||||
|
| `complex.delete_unit` | 删除楼栋/单元/房号 | boolean | `false` | 是否可删除楼栋、单元、房号 | True | False | True | 1 |
|
||||||
|
| `complex.delete_with_property` | 删除楼盘数据(含房源) | boolean | `false` | 是否可无视房源直接删除楼盘及以下所有数据 | True | False | True | 1 |
|
||||||
|
| `complex.merge` | 合并楼盘 | boolean | `false` | 是否可合并不同层级楼盘数据 | True | False | True | 1 |
|
||||||
|
| `complex.move_unit` | 移动楼栋/单元/房号数据 | boolean | `false` | 是否可将楼栋单元数据移动至其他楼盘 | True | False | True | 1 |
|
||||||
|
| `complex.lock` | 锁定/解锁楼盘 | boolean | `false` | 是否可操作锁定或解锁楼盘 | True | False | True | 1 |
|
||||||
|
| `complex.view_deal` | 楼盘挂牌成交数据 | boolean | `false` | 是否显示楼盘挂牌及成交数据 | True | False | True | 1 |
|
||||||
|
| `complex.view_deal_detail` | 司内成交明细及套数 | boolean | `false` | 是否显示公司成交房源明细信息及套数 | True | False | True | 1 |
|
||||||
|
| `complex.view_address_scope` | 楼街房源地址数据查看范围 | scope | `self` | 控制查看部门内其他员工楼街房源地址数据:本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `complex.region_manage` | 区域管理 | boolean | `false` | 是否可对区域商圈进行新增、合并、关联操作 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 楼盘资料
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `complex.material.view_photo` | 楼盘照片 | boolean | `false` | 是否显示楼盘照片列表 | True | False | True | 1 |
|
||||||
|
| `complex.material.manage_photo` | 管理照片 | boolean | `false` | 是否可上传照片、设为封面 | True | False | True | 1 |
|
||||||
|
| `complex.material.delete_photo` | 删除照片 | boolean | `false` | 是否可删除楼盘照片 | True | False | True | 1 |
|
||||||
|
| `complex.material.download_photo` | 下载照片 | boolean | `false` | 是否可下载楼盘照片 | True | False | True | 1 |
|
||||||
|
| `complex.material.view_attachment` | 楼盘附件 | boolean | `false` | 是否显示楼盘附件模块 | True | False | True | 1 |
|
||||||
|
| `complex.material.manage_attachment` | 管理附件 | boolean | `false` | 是否可上传楼盘附件 | True | False | True | 1 |
|
||||||
|
| `complex.material.download_attachment` | 下载附件 | boolean | `false` | 是否可下载楼盘附件 | True | False | True | 1 |
|
||||||
|
| `complex.material.delete_attachment` | 删除附件 | boolean | `false` | 是否可删除楼盘附件 | True | False | True | 1 |
|
||||||
|
| `complex.material.view_surrounding` | 周边配套 | boolean | `false` | 是否显示周边配套模块 | True | False | True | 1 |
|
||||||
|
|
||||||
|
### 楼盘反馈
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| `complex.feedback.view` | 楼盘反馈列表 | scope | `self` | 可查看小区反馈列表的数据范围:本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `complex.feedback.handle` | 楼盘反馈处理 | boolean | `false` | 是否可处理或不予处理楼盘反馈 | True | False | True | 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 组织人事-组织模块(21 条)
|
||||||
|
|
||||||
|
### 组织管理
|
||||||
|
|
||||||
|
| permission_code | display_name | value_type | default_value | description | is_active | is_deprecated | is_system | version |
|
||||||
|
| --------------------------------- | ------------- | ---------- | ------------- | ----------------------------- | --- | --- | --- | --- |
|
||||||
|
| `org.view_structure` | 组织结构查看 | scope | `self` | 控制组织结构页面的部门/员工查看范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `org.view_dept` | 部门查看 | boolean | `false` | 是否可查看部门信息 | True | False | True | 1 |
|
||||||
|
| `org.edit_dept` | 部门维护 | boolean | `false` | 是否可对部门进行编辑操作 | True | False | True | 1 |
|
||||||
|
| `org.view_staff` | 员工查看 | boolean | `false` | 是否可查看员工详情 | True | False | True | 1 |
|
||||||
|
| `org.edit_staff` | 员工维护 | boolean | `false` | 是否可进行员工异动、批量设置员工上级等操作 | True | False | True | 1 |
|
||||||
|
| `org.edit_staff_detail` | 员工详情编辑 | boolean | `false` | 是否可编辑员工信息、新增奖惩记录、编辑账号信息 | True | False | True | 1 |
|
||||||
|
| `org.freeze_account` | 员工账号冻结/解冻 | boolean | `false` | 是否可冻结/解冻员工账号 | True | False | True | 1 |
|
||||||
|
| `org.import_staff` | 批量导入员工 | boolean | `false` | 是否可批量导入员工 | True | False | True | 1 |
|
||||||
|
| `org.export_staff` | 导出员工 | boolean | `false` | 是否可导出员工数据 | True | False | True | 1 |
|
||||||
|
| `org.view_permission` | 员工权限查看 | boolean | `false` | 是否可查看员工权限配置 | True | False | True | 1 |
|
||||||
|
| `org.edit_permission` | 员工权限设置 | boolean | `false` | 是否可编辑员工权限 | True | False | True | 1 |
|
||||||
|
| `org.export_permission` | 权限管理页面导出 | boolean | `false` | 是否可在权限管理页面导出员工角色及管理范围数据 | True | False | True | 1 |
|
||||||
|
| `org.edit_position` | 职务维护 | boolean | `false` | 是否可新增/编辑/删除/合并员工职务 | True | False | True | 1 |
|
||||||
|
| `org.edit_role` | 角色维护 | boolean | `false` | 是否可展示角色管理页面并进行角色编辑 | True | False | True | 1 |
|
||||||
|
| `org.view_store_list` | 门店列表查看 | boolean | `false` | 是否可查看门店列表 | True | False | True | 1 |
|
||||||
|
| `org.export_store_list` | 门店列表导出 | boolean | `false` | 是否可导出门店列表 | True | False | True | 1 |
|
||||||
|
| `org.view_contact_book` | 员工通讯录查看 | scope | `self` | 控制查看员工通讯录的范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `org.transfer_business` | 转移业务归属 | scope | `none` | 控制转出/转入人的可选范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `org.resign_apply` | 离职申请范围 | scope | `self` | 控制员工离职申请范围:无/本人/本部/全部 | True | False | True | 1 |
|
||||||
|
| `org.invite_onboard` | 入职邀请 | boolean | `false` | 是否可生成入职邀请链接/二维码 | True | False | True | 1 |
|
||||||
|
| `org.view_contact_phone_limit` | 每日查看通讯录电话次数上限 | integer | `0` | 每天可查看员工通讯录电话次数,0=不允许,-1=不限制 | True | False | True | 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 汇总
|
||||||
|
|
||||||
|
| 模块 | 条数 |
|
||||||
|
| -------------- | ------- | --- | --- | --- | --- |
|
||||||
|
| property(房源) | 66 |
|
||||||
|
| client(客源) | 36 |
|
||||||
|
| home(首页) | 4 |
|
||||||
|
| complex(楼盘/小区) | 27 |
|
||||||
|
| org(组织人事-组织) | 21 |
|
||||||
|
| **合计** | **154** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 公共元数据(所有记录相同)
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
| --------------- | ---------------------------------------------------- |
|
||||||
|
| `is_active` | `True` |
|
||||||
|
| `is_deprecated` | `False` |
|
||||||
|
| `is_system` | `True` |
|
||||||
|
| `version` | `1` |
|
||||||
|
| `schema` | `public`(SHARED_APPS,migration 在 public schema 运行一次) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration 分批策略
|
||||||
|
|
||||||
|
| 批次 | Migration 文件名 | 涵盖模块 | 条数 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 1 | `0002_seed_permission_property.py` | property | 66 |
|
||||||
|
| 2 | `0003_seed_permission_client.py` | client | 36 |
|
||||||
|
| 3 | `0004_seed_permission_home_complex_hr.py` | home + complex + hr | 52 |
|
||||||
|
|
||||||
|
> 每个 migration 包含 `forwards` 和 `backwards` 函数,`backwards` 按 code 批量删除,保证可回滚。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 内置角色 role_permissions 分配(最大权限角色)
|
||||||
|
|
||||||
|
**策略**:内置「最大权限角色」对以上全部 154 条 PermissionDef 设置最大开放值:
|
||||||
|
- `boolean` 类型 → `true`
|
||||||
|
- `scope` 类型 → `all`(全部)
|
||||||
|
- `integer` 类型 → `-1`(不限制)
|
||||||
|
|
||||||
|
其余 2 个内置角色(高级业务员 / 分行经理)不预置 role_permissions,由租户管理员自行配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统管理模块说明(本批不纳入)
|
||||||
|
|
||||||
|
系统管理(Admin Console)面向 Platform Super Admin / Ops Operator,这两个角色在 `public` schema 使用独立认证体系,不走租户 RBAC。
|
||||||
|
访问控制在 view 层直接用 Django `is_staff` / `is_superuser` 或平台角色装饰器控制,无需 PermissionDef 记录。
|
||||||
|
如后续 Platform Admin 角色扩展到 3 个以上且需要功能细分,再单独追加一版 migration。
|
||||||
138
Project/fonrey/PRD/权限管理/客源.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
**客源** 开启才可使用相关功能;关闭后会去去相关系统所有权限,并隐藏对应菜单入口。 本模块开启 ●
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 私客基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| -------------- | ---------- | ------------------ |
|
||||||
|
| 新增私客 | true/false | |
|
||||||
|
| 个人私客数量上限 | 999 | 999=不限制,0=不允许 |
|
||||||
|
| 查看私客(非保护客) | 无/本人/本部/全部 | |
|
||||||
|
| 查看私客跟进范围(非保护客) | 无/本人/本部/全部 | 控制查看非保护客的私客跟进范围 |
|
||||||
|
| 查看私客跟进范围(保护客) | 无/本人/本部/全部 | 控制查看保护客的私客跟进范围 |
|
||||||
|
| 私客转公客 | 无/本人/本部/全部 | |
|
||||||
|
| 查看私客(保护客) | 无/本人/本部/全部 | 查看什么经纪人范围的保护客 |
|
||||||
|
| 编辑私客(非保护客) | 无/本人/本部/全部 | 编辑什么员工范围的非保护客的私客信息 |
|
||||||
|
| 编辑私客(保护客) | 无/本人/本部/全部 | 编辑什么员工范围的保护客的私客信息 |
|
||||||
|
| 设置/取消保护客 | 无/本人/本部/全部 | 设置/取消客源归属人的保护客的范围 |
|
||||||
|
| 私客(非保护客)听录音 | 无/本人/本部/全部 | 私客(非保护客)听录音的范围 |
|
||||||
|
| 私客(保护客)听录音 | 无/本人/本部/全部 | 私客(保护客)听录音的范围 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 公客基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------------- | ---------- | ----------------------------------------- |
|
||||||
|
| 公客查看范围 | 无/本部/全部 | 控制公客查看范围 |
|
||||||
|
| 公客查看跟进 | 无/本部/全部 | |
|
||||||
|
| 公客转私客 | true/false | 跟公客查看范围相关。若启用,则可对查看范围内的公客转私客;若关闭,无法将公客转私客 |
|
||||||
|
| 查看公转私审批中客户 | 无/本部/全部 | |
|
||||||
|
| 改公客状态 | true/false | |
|
||||||
|
| 编辑公客 | true/false | |
|
||||||
|
| 公客听录音 | 无/本部/全部 | 公客听录音的范围 |
|
||||||
|
| 公客详情页报备记录查看范围 | 无/本部/全部 | 以报备人为准,控制登录人可以看到公客详情页中的哪些报备记录 |
|
||||||
|
## 成交客基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ----------- | ---------- | --------------------------------------------------- |
|
||||||
|
| 查看成交客(私客类型) | 无/本人/本部/全部 | 控制查看归属人为个人的成交客范围 |
|
||||||
|
| 查看成交客(公客类型) | 无/本人/本部/全部 | 控制查看归属人为共享账号的成交客范围,查看权限为本部时,支持查看本级及以上部门的共享账号的成交客 |
|
||||||
|
| 成交客查看跟进 | 无/本人/本部/全部 | |
|
||||||
|
| 成交客再次租/购 | true/false | 跟成交客查看范围相关。若启用,则可对查看范围内的成交客操作再次租/购;若关闭,无法将成交客转再次租/购 |
|
||||||
|
| 成交客编辑来源 | 无/本人/本部/全部 | 控制管理者编辑不同人员范围的成交客来源字段 |
|
||||||
|
| 成交客听录音 | 无/本人/本部/全部 | 成交客听录音 |
|
||||||
|
| 导出成交客列表 | ○ | 成交客支持导出列表 |
|
||||||
|
| | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 联系人号码权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------------------- | ---------- | ------------------------------------------ |
|
||||||
|
| 私客(非保护客)&成交客查看号码 | 无/本人/本部/全部 | 控制查看非保护客的私客、成交客的号码范围 |
|
||||||
|
| 成交客查看号码 | 无/本人/本部/全部 | 控制查看成交客的号码范围,权限为本部时,支持查看本级及以上部门的共享账号的成交客号码 |
|
||||||
|
| 私客(保护客)查看号码 | 无/本人/本部/全部 | 控制查看保护客的号码范围 |
|
||||||
|
| 营销客/私客/成交客【联系人号码】查看个数 | 999 | 999=不限制,0=不允许 |
|
||||||
|
| 查看【公客】联系人号码 | 无/本人/本部/全部 | 控制公客查看号码范围 |
|
||||||
|
| 公客【联系人号码】查看个数 | 999 | 999=不限制,0=不允许 |
|
||||||
|
| 查看资料客电话个数 | 999 | 每天可查看资料客真实号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 拨打私客(非保护客)电话 | 无/本人/本部/全部 | 控制拨打非保护客的私客号码范围 |
|
||||||
|
| 拨打私客(保护客)电话 | 无/本人/本部/全部 | 控制拨打保护客的私客号码范围 |
|
||||||
|
| 成交客拨打电话 | 无/本人/本部/全部 | 控制拨打成交客的私客号码范围 |
|
||||||
|
| 公客拨打电话 | 无/本人/本部/全部 | 控制经纪人拨打公客号码范围 |
|
||||||
|
| 编辑私客(非保护客)&成交客联系人 | 无/本人/本部/全部 | 控制编辑非保护客、成交客联系人非号码信息范围,有权时可新增联系人 |
|
||||||
|
| 编辑私客(保护客)联系人 | 无/本人/本部/全部 | 控制编辑保护客联系人非号码信息范围,有权时可新增联系人 |
|
||||||
|
| 公客编辑联系人 | 无/本人/本部/全部 | 控制编辑公客联系人非号码信息范围,有权时可新增联系人 |
|
||||||
|
| 编辑私客(非保护客)&成交客联系人号码 | 无/本人/本部/全部 | 控制编辑非保护客、成交客联系人号码范围 |
|
||||||
|
| 编辑私客(保护客)联系人号码 | 无/本人/本部/全部 | 控制编辑保护客联系人号码范围 |
|
||||||
|
| 公客编辑联系人号码 | 无/本人/本部/全部 | 控制经纪人编辑公客号码范围 |
|
||||||
|
## 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---------------- | ---------- | ----------------------------- |
|
||||||
|
| 删除客源(查看已删除客源) | true/false | |
|
||||||
|
| 不受公客查看号码/拨打次数限制 | true/false | |
|
||||||
|
| 手动客源转为成交客 | true/false | |
|
||||||
|
| 单个客源修改相关员工 | 无/本人/本部/全部 | 可修改什么范围内客源的相关方,包括添加合作人 |
|
||||||
|
| 批量客源修改相关员工 | 无/本人/本部/全部 | |
|
||||||
|
| 批量修改私客、公客来源 | true/false | |
|
||||||
|
| 查看客户/联系人操作日志 | true/false | 若启用,可查看客户详情页手机号修改/删除、客户合并等记录。 |
|
||||||
|
| 不受资料客查看号码/拨打次数限制 | true/false | |
|
||||||
|
| 隐藏客源跟进 | true/false | 开启开关后,可隐藏客源跟进,且查看被隐藏的跟进 |
|
||||||
|
| 私客列表导出 | true/false | 是否支持导出私客列表信息 |
|
||||||
|
| 置顶客源跟进 | true/false | 开启后,可置顶/取消置顶 客源跟进 |
|
||||||
|
| 查看客户号码时,无需强制写跟进 | true/false | 若启用,查看客户号码不需要强制写跟进 |
|
||||||
|
| 修改首录人 | true/false | 是否允许修改客源的首录人 |
|
||||||
|
| 搜索离职员工 | true/false | 是否允许搜索离职员工信息 |
|
||||||
|
| 给员工写待办 | 无/本部/全部 | 控制为哪些人员写待办 |
|
||||||
|
| 置顶员工的客户 | 无/本部/全部 | 控制置顶客户范围 |
|
||||||
|
| 允许合并自己的私客 | true/false | 开启后,可合并归属人为登录人本人的私客 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 空看 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------------- | ---------- | --------- |
|
||||||
|
| 空看/踩盘单中楼栋单元房号查看 | 无/本人/本部/全部 | 以空看/踩盘人为准 |
|
||||||
|
| 空看/踩盘单查看 | 无/本人/本部/全部 | 以空看/踩盘人为准 |
|
||||||
|
| 空看/踩盘单中附件查看 | 无/本人/本部/全部 | 以上传人为准 |
|
||||||
|
## 带看/预约权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------------- | ---------- | ------------------- |
|
||||||
|
| 带看/预约新增 | true/false | |
|
||||||
|
| 带看/预约单中楼栋单元房号查看 | 无/本人/本部/全部 | 以带看人为准 |
|
||||||
|
| 带看/预约编辑、作废 | 无/本人/本部/全部 | 以带看人为准 |
|
||||||
|
| 私客、成交客详情页带看单查看 | 无/本人/本部/全部 | 以带看人为准 |
|
||||||
|
| 带看单中附件查看 | 无/本人/本部/全部 | 以上传人为准 |
|
||||||
|
| 公客客源详情页带看单查看 | 无/本人/本部/全部 | 以带看人为准,同时对列表页和详情页生效 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 营销客 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------- | ---------- | ---------------------------- |
|
||||||
|
| 来电通&营销客管理 | 无/本人/本部/全部 | 查看巧客力、来电通的营销客范围,包括查看营销客列表、详情 |
|
||||||
|
| 营销客看板 | 无/本人/本部/全部 | 查看营销客看板的范围 |
|
||||||
|
| 来电通通话列表查看 | 无/本人/本部/全部 | 控制员工查看通话列表的数据范围 |
|
||||||
|
| 听录音 | true/false | 若开启,可以听录音 |
|
||||||
|
| 查看明码 | 无/本人/本部/全部 | 若开启,可以查看客户号码 |
|
||||||
|
| 查看账单 | true/false | 若开启,可以查看来电通账单 |
|
||||||
|
| 营销客拉私 | 无/本人/本部/全部 | 允许为什么范围的营销客拉私 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 资料客 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------- | ---------- | ------------------------------------ |
|
||||||
|
| 查看资料客 | 无/本人/本部/全部 | 按照归属人所在范围进行查看,本部权限时支持客源行政跨部权限内设置的跨部门 |
|
||||||
|
| 导入资料客 | true/false | 若启用,可导入资料客 |
|
||||||
|
| 查看号码 | true/false | 若启用,可查看号码 |
|
||||||
|
| 删除资料客 | true/false | 若启用,可删除资料客 |
|
||||||
|
| 查看资料客操作日志 | 无/本人/本部/全部 | 查看什么员工范围的资料客操作日志 |
|
||||||
50
Project/fonrey/PRD/权限管理/小区.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
## 楼盘管理 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---------------------- | ---------- | --------------------------------------------------------------------- |
|
||||||
|
| 楼盘管理查看 | true/false | 关闭后,则不显示楼盘管理系统模块 |
|
||||||
|
| 楼盘结构查看 | true/false | 开启后,可以查看楼栋-单元-房号数据 |
|
||||||
|
| 新增/批量新增楼盘 | true/false | 允许新增楼盘 |
|
||||||
|
| 新增/批量新增楼栋、单元、房号 | true/false | 新增楼栋、单元、房号数据 |
|
||||||
|
| 编辑楼盘 | true/false | 编辑楼盘 |
|
||||||
|
| 编辑楼栋/单元/房号信息 | true/false | 编辑楼栋、单元、房号信息 |
|
||||||
|
| 关联标准库/取关 | true/false | 若启用,则可关联至标准楼盘、标准楼栋、单元、房号 |
|
||||||
|
| 删除楼盘 | true/false | 删除楼盘 |
|
||||||
|
| 删除楼盘数据(一并删除房源) | true/false | 若启用,则无视是否存在房源,对不同层级及以下的数据全部删除 |
|
||||||
|
| 删除楼栋、单元、房号 | true/false | 删除楼栋、单元、房号 |
|
||||||
|
| 合并楼盘 | true/false | 若启用,则可合并不同层级楼盘数据(楼栋、单元、房号) |
|
||||||
|
| 移动楼栋/单元/房号数据 | true/false | 若启用,则可将A楼盘楼栋单元及以下数据移动至B楼盘;转移房号不能跨小区进行转移; |
|
||||||
|
| 锁定/解锁楼盘 | true/false | 操作锁定/解锁楼盘 |
|
||||||
|
| 楼街房源地址数据查看范围 | 本人/本部/全部 | 设置员工能否查看部门内其他员工的楼街房源地址数据 |
|
||||||
|
| 楼盘挂牌成交数据 | true/false | 开启后,显示楼盘挂牌及成交数据信息 |
|
||||||
|
| 司内成交明细及套数 | true/false | 开启后,显示公司成交的房源明细信息及成交套数 |
|
||||||
|
| 区域管理 | true/false | 若启用,则可对区域商圈进行新增、合并、关联操作 |
|
||||||
|
| 查看销控盘 | true/false | 开启后,可在楼盘管理系统-楼盘里,查看销控盘。请注意:员工查看销控盘时房源地址是直接可见的,建议只给管理层开启!!! |
|
||||||
|
| 查看销控盘时,只可查看本部门作业范围内的楼盘 | true/false | 开启后,只可查看本部门作业范围内的楼盘的销控盘;关闭后,则跟作业范围无关,「查看销控盘」权限开启即可见所有楼盘的销控盘;Tenant Admin(租户管理员)不受限制 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 楼盘资料管理 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------ | ---------- | ----------------------- |
|
||||||
|
| 楼盘照片 | true/false | 开启后,显示楼盘照片列表 |
|
||||||
|
| 管理照片 | true/false | 楼盘管理系统-楼盘照片,包含上传照片、设为封面 |
|
||||||
|
| 删除照片 | true/false | 允许删除照片 |
|
||||||
|
| 下载照片 | true/false | 允许下载照片 |
|
||||||
|
| 楼盘附件 | true/false | 开启后,显示楼盘附件模块 |
|
||||||
|
| 管理附件 | true/false | 允许上传楼盘附件 |
|
||||||
|
| 下载附件 | true/false | 允许下载楼盘附件 |
|
||||||
|
| 删除附件 | true/false | 允许删除楼盘附件 |
|
||||||
|
| 周边配套 | true/false | 开启后,显示周边配套模块 |
|
||||||
|
| 学校管理列表 | true/false | 开启后,显示楼盘管理系统中的学校管理列表 |
|
||||||
|
| 学校管理 | true/false | 包含新增、编辑、删除 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 楼盘处理 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------ | ---------- | -------------- |
|
||||||
|
| 楼盘反馈列表 | 本人/本部/全部 | 可查看小区反馈列表的数据范围 |
|
||||||
|
| 楼盘反馈处理 | true/false | 包含处理、不予处理操作 |
|
||||||
297
Project/fonrey/PRD/权限管理/房源-二手租赁.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
## 房源基础
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------------------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 新增房源 | true/false | 若启用,则可新增房源 |
|
||||||
|
| 状态查看范围 | 出租/出售/暂缓/他售/他租/无效/我售/我租/删除/不租/不售/验真超时 | 若选择,则可查看被选中状态的房源,可多选 |
|
||||||
|
| 用途查看范围 | 住宅/商住/别墅/写字楼/商铺/其他 | 若选择,则可查看被选中用途的房源,可多选 |
|
||||||
|
| 将房源属性改为公盘 | true/false | 若启用,则可将房源属性改为公盘 |
|
||||||
|
| 将房源属性改为私盘 | true/false | 若启用,则可将房源属性改为私盘 |
|
||||||
|
| 新增/编辑最高可设置的标签类别 | A/B/C/D/E | 用户最高可以给房源打哪些分类的标签 |
|
||||||
|
| 成交房源列表及价格信息显示 | true/false | 若开启,可查看成交房源列表、价格解读历史成交记录明细、房源详情页成交信息 |
|
||||||
|
| 维护房源列表 | 无/本人/本部/全部 | 按照维护人的范围查看维护房源列表 |
|
||||||
|
| 价格解读 | true/false | 若启用,可以查看房源详情页的价格解读 |
|
||||||
|
| 查看同业主其他房源 | true/false | 若开启,则可在房源详情页查看同业主房源 |
|
||||||
|
| 联系房源相关方时,电话产品的使用要求 | 必须使用电话产品/非本部员工必须使用电话产品/不使用电话产品 | 选择「必须使用电话产品」则须使用电话产品联系;选择「非本部员工须使用电话产品」则本部员工号码可直接查看、非本部员工须使用电话产品联系;选择「不使用电话产品」则可直接查看号码;(请注意:若巧房移动端拨号方式设置了可明码拨号,可明码拨号) |
|
||||||
|
| 查看房源挂牌历史 | true/false | 若开启,可以查看房源详情页的房源挂牌历史 |
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------------------------ | ----------- | ---------------------------------------------------------------------- |
|
||||||
|
| 删除房源 | true/false | 若启用,则可删除房源 |
|
||||||
|
| 恢复已删除房源 | true/false | 若启用,则可恢复已删除的房源 |
|
||||||
|
| 修改为我售/我租状态 | true/false | 若启用,可修改房源状态为我租/我售,同时若设置了我租我售保护期,房源将被隐藏 |
|
||||||
|
| 修改房源是否为保护房 | true/false | 若启用,则可修改房源的保护设置 |
|
||||||
|
| 查看保护期内房源 | 我租/我售/已售/已租 | 若选择,则可查看被选中状态的保护期内房源,可多选 |
|
||||||
|
| 将房源属性改为封盘 | true/false | 若启用,则可将房源属性改为封盘 |
|
||||||
|
| 将房源属性改为特盘 | true/false | 若启用,则可将房源属性改为特盘 |
|
||||||
|
| 将房源等级设为A | true/false | 可将挂牌中房源等级设为A(急迫) |
|
||||||
|
| 将房源等级设为E | true/false | 可将房源等级设为E(暂不关注) |
|
||||||
|
| 批量修改相关方-可修改范围 | 无/本部/全部 | 房源列表页,可批量修改的原相关方范围。举例,若设置本部,那么小王能修改他所在部门员工的房源相关方 |
|
||||||
|
| 修改相关方-可修改范围 | 无/本部/全部 | 房源详情页,可修改的原相关方范围。举例,若设置本部,那么小王能在房源详情页修改他所在部门员工的房源相关方 |
|
||||||
|
| 批量修改相关方/修改相关方,可修改的新相关方范围 | 无/本部/全部 | 房源列表的批量修改相关方、房源详情页修改相关方,可将新相关方修改给谁 |
|
||||||
|
| 修改新相关方时,可选择到已离职员工 | true/false | 若开启,房源列表&房源详情页修改的新相关方,可选择到已离职员工 |
|
||||||
|
| 相关方权限范围 | 无/本部/全部 | 设置员工能否拥有部门内其他员工的相关方权限 |
|
||||||
|
| 调价无需业主短信确认和调价审批 | true/false | 开启后,员工可直接调价成功,无需业主短信确认和调价审批 |
|
||||||
|
| 取消隐盘 | 无/本部/全部 | 可取消隐盘的房源对应的范围 |
|
||||||
|
| 修改房屋介绍信息 | true/false | 若启用,则可修改房源的营销标题、核心卖点、户型介绍、小区介绍、业主心态 |
|
||||||
|
| 房源列表数据导出 | true/false | 若启用,则可将房源列表可视范围内全量数据导出 |
|
||||||
|
| 小区房号搜索是否受房源地址权限控制 | true/false | 若启用,搜索小区房号时,如果操作人没有查看房源地址的权限,则不进搜索结果 |
|
||||||
|
| 不受作业共享范围限制 | true/false | 开启后,不受作业范围和共享范围配置限制,在【房源列表】作业盘、共享盘范围筛选和【查看作业范围】页面中可查看全公司作业盘及共享盘数据及配置情况 |
|
||||||
|
| 作业范围配置 | true/false | 控制是否可以进行作业范围配置 |
|
||||||
|
| 修改品质好房推荐人 | true/false | 控制是否可修改的品质好房推荐人范围,若权限为关,则默认推荐人为操作人 |
|
||||||
|
| 设置维护人 | true/false | 当房源维护人为空时,即在维护共享池,可直接设置全公司任意员工为维护人 |
|
||||||
|
| 重复房源合并 | true/false | 若启用,有权限合并重复房源 |
|
||||||
|
| 不受新增激活验真中房源不可见限制 | true/false | 开启后,无视新增激活验真中房源不可见限制,可以在房源列表可见,详情页可访问 |
|
||||||
|
| 查看疑似问题号码房源列表 | true/false | 若开启,则可查看疑似问题号码房源列表 |
|
||||||
|
| 业主服务报告制作 | true/false | 若开启,可以制作业主服务报告 |
|
||||||
|
### 房源置顶/取消置顶 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---- | --- | ------------------- |
|
||||||
|
| 置顶天数 | 999 | 设置房源置顶天数,建议最多设置60天 |
|
||||||
|
| 置顶套数 | 999 | 设置房源置顶套数,建议最多设置100套 |
|
||||||
|
|
||||||
|
## 房源核心信息
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------------------ | --- | ----------------------------------- |
|
||||||
|
| 查看业主/联系人号码次数 | 999 | 每天可查看房源真实号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 查看出售房源业主/联系人号码次数 | 999 | 每天可查看出售房源真实号码总次数;999=不限制,0=不允许 |
|
||||||
|
| 查看出租房源业主/联系人号码次数 | 999 | 每天可查看出租房源真实号码总次数;999=不限制,0=不允许 |
|
||||||
|
| 查看未挂牌房源业主/联系人号码次数 | 999 | 每天可查看未挂牌房源真实号码总次数;999=不限制,0=不允许 |
|
||||||
|
| 拨打业主/联系人号码次数 | 999 | 每天可拨打房源业主/联系人号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 拨打出售房源业主/联系人号码次数 | 999 | 每天可拨打出售房源业主/联系人号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 拨打出租房源业主/联系人号码次数 | 999 | 每天可拨打出租房源业主/联系人号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 拨打未挂牌房源业主/联系人号码次数 | 999 | 每天可拨打未挂牌房源业主/联系人号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 查看楼栋/单元/楼层/房号总次数 | 999 | 每天可查看房源真实地址总次数;999=不限制,0=不允许 |
|
||||||
|
| 查看出售楼栋/单元/楼层/房号次数 | 999 | 每天可查看出售房源真实地址总次数;999=不限制,0=不允许 |
|
||||||
|
| 查看出租楼栋/单元/楼层/房号次数 | 999 | 每天可查看出租房源真实地址总次数;999=不限制,0=不允许 |
|
||||||
|
| 查看未挂牌楼栋/单元/楼层/房号次数 | 999 | 每天可查看未挂牌房源真实地址总次数;999=不限制,0=不允许 |
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| -------------------------------- | ------------------ | --------------------------------------------------------------------------------------------- |
|
||||||
|
| 修改楼栋/单元/楼层/房号 | true/false | 若启用,则可修改楼栋/单元/房号/楼层 |
|
||||||
|
| 新增业主/联系人 | true/false | 若启用,则可新增业主/联系人 |
|
||||||
|
| 修改业主核心信息 | 无/本人/本部/全部 | 设置修改业主核心信息的数据权限范围:电话1,电话2,微信号,QQ |
|
||||||
|
| 修改业主非核心信息 | 无/本人/本部/全部 | 设置修改业主非核心信息的数据权限范围:姓名,身份,称呼,备注 |
|
||||||
|
| 删除业主/联系人 | true/false | 若启用,则可删除业主/联系人 |
|
||||||
|
| 标记/取消标记已成交业主 | true/false | 若启用则可以标记或者取消标记已成交业主 |
|
||||||
|
| 业主/联系人模块,查看隐号业主的方式 | 直接查看/点击「查看信息」后才可查看 | 若选择"直接查看",则房源详情页的业主号码查看/跟进查看通话信息(含列表/小详情),将直接展示隐号业主;该情况下,经纪人可能通过号码的前3后2位,推测到业主/地址。请谨慎使用「直接查看」 |
|
||||||
|
| 查看业主/联系人操作日志 | true/false | 若启用,可查看业主/联系人的新增、修改、删除等记录。 |
|
||||||
|
| 设置禁止/允许联系业主/联系人 | 无/本人/本部/全部 | 可以禁止/允许联系业主/联系人的房源范围,设置禁止联系后所有人不能查看号码、不能使用电话系统联系该房源业主/联系人 |
|
||||||
|
| 查看产证信息 | true/false | 若开启则可查看房源详情页产证信息 |
|
||||||
|
| 查看委托人/产权人证件号及电话 | 无/本人/本部/全部 | 可根据委托方所在范围查看委托人/产权人的证件号及电话 |
|
||||||
|
| 不受房源拨打/查看号码次数限制 | true/false | 若开启,则拨打/查看号码次数不受权限配置次数限制 |
|
||||||
|
| 管理隐盘权限 | 无/本人/本部/全部 | 可以在隐盘情况下查看号码、联系业主、查看地址;且可以设置隐盘。 |
|
||||||
|
| 不受查看房源地址次数限制的范围 | 无/本人/本部/全部 | 针对维护人(租维护人/售维护人/维护人),可以设定哪些范围内的房源地址查看不受次数限制 |
|
||||||
|
| 置顶业主/联系人 | 无/本人/本部/全部 | 若启用,则可以置顶业主/联系人 |
|
||||||
|
| 仅维护人查看有效房源业主模式下,有维护人的房源查看业主的数据范围 | 无/本人/本部/全部 | 仅维护人查看有效房源业主模式下,有维护人的房源查看业主的数据范围 |
|
||||||
|
| 仅维护人拨打有效房源业主模式下,有维护人的房源拨打业主的数据范围 | 无/本人/本部/全部 | 仅维护人拨打有效房源业主模式下,有维护人的房源拨打业主的数据范围 |
|
||||||
|
| 仅维护人查看有效房源业主模式下,共享池房源查看业主的数据范围 | 无/本人/本部/全部 | 仅维护人查看有效房源业主模式下,共享池房源查看业主的数据范围 |
|
||||||
|
| 仅维护人拨打有效房源业主模式下,共享池房源拨打业主的数据范围 | 无/本人/本部/全部 | 仅维护人拨打有效房源业主模式下,共享池房源拨打业主的数据范围 |
|
||||||
|
| 是否可查看和拨打【纠错中-新业主】的全部号码 | 无/本人/本部/全部 | 【查看号码】&还有查看次数时,按照「添加该新号码的员工」的范围判断是否展示和拨打纠错中-新业主】的全部号码 |
|
||||||
|
## 房源钥匙
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ----------- | ---------- | ------------------------------------------------ |
|
||||||
|
| 新增钥匙 | true/false | 若启用,则可新增钥匙 |
|
||||||
|
| 修改钥匙 | 无/本人/本部/全部 | 按照钥匙方所在范围进行修改钥匙 |
|
||||||
|
| 退还钥匙 | 无/本人/本部/全部 | 按照钥匙方所在范围控制是否可退还钥匙给业主 |
|
||||||
|
| 查看机械锁附件 | 无/本人/本部/全部 | 按照钥匙方或提交人所在部门来判断是否有查看机械锁新增、修改、退回提交的钥匙附件权限。 |
|
||||||
|
| 查看密码锁附件 | 无/本人/本部/全部 | 按照钥匙方或提交人所在部门来判断是否有权限查看密码锁新增、修改、退回提交的钥匙附件权限。 |
|
||||||
|
| 查看钥匙录音 | 无/本人/本部/全部 | 按照钥匙方或提交人所在部门来判断是否有权限 |
|
||||||
|
| 查看钥匙密码 | 无/本人/本部/全部 | 按照钥匙方所在范围控制是否可查看密码 |
|
||||||
|
| 查看钥匙编号 | 无/本人/本部/全部 | 按照钥匙方所在范围控制是否可查看钥匙编号 |
|
||||||
|
| 钥匙借出 | 无/本人/本部/全部 | 可按钥匙保管部门所在范围借钥匙 |
|
||||||
|
| 钥匙归还 | 无/本人/本部/全部 | 可按钥匙保管部门所在范围归还钥匙 |
|
||||||
|
| 查看钥匙借出和归还附件 | 无/本人/本部/全部 | 本人指附件上传人或钥匙方;本部指附件上传人或钥匙方所在部门、以及钥匙管理部门的员工;全部指全司。 |
|
||||||
|
| 归还钥匙操作 | 移动端/电脑端 | 控制经纪人是否可在电脑端/移动端操作归还钥匙,电脑端直接点击即可退还,移动端需要定位打卡拍照退还 |
|
||||||
|
| 钥匙在他司备注 | true/false | 若开启,可新增/编辑钥匙在他司备注 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ----------------- | ---------- | --------------------------------------------------------------------- |
|
||||||
|
| 钥匙查看范围 | 无/本部/全部 | 配置可查看钥匙的具体范围 |
|
||||||
|
| 删除钥匙范围 | 无/本人/本部/全部 | 按照钥匙方所在范围进行删除 |
|
||||||
|
| 钥匙管理列表,展示机械锁的钥匙编号 | 明文展示/不展示 | 选择「明文展示」,则钥匙管理列表明文展示钥匙编号(请注意:需同时打开「查看钥匙编号」权限才可生效);选择「不展示」,点击隐码后才能查看编号 |
|
||||||
|
| 钥匙列表数据导出 | true/false | 若开启,可导出钥匙数据 |
|
||||||
|
## 房源实勘
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---------- | ---------- | ------------------------- |
|
||||||
|
| 新增图片 | true/false | 若启用,则可新增图片 |
|
||||||
|
| 修改图片 | 无/本人/本部/全部 | 可按图片的上传人所在范围进行修改图片 |
|
||||||
|
| 下载图片 | true/false | 若启用,则可下载图片 |
|
||||||
|
| 新增实勘 | true/false | 若启用,则可新增实勘 |
|
||||||
|
| 查看实勘 | true/false | 若启用,则可查看实勘 |
|
||||||
|
| 新增预约拍摄 | true/false | 若启用,则可发起预约拍摄 |
|
||||||
|
| 查看预约拍摄列表 | 无/本人/本部/全部 | 按照预约发起人的范围查看预约拍摄列表 |
|
||||||
|
| 查看平台实勘拍摄列表 | 无/本人/本部/全部 | 按照预约发起人的范围查看预约拍摄列表 |
|
||||||
|
| 上传视频 | true/false | 若启用,则可上传视频 |
|
||||||
|
| 下载视频 | true/false | 若启用,则可下载视频 |
|
||||||
|
| 播放视频 | true/false | 若启用,则可播放视频 |
|
||||||
|
| 新增二手房实地核验 | true/false | 开启为经纪人、摄影师均可拍摄;关闭仅允许摄影师拍摄 |
|
||||||
|
| 新增租房实地核验 | true/false | 开启为经纪人、摄影师均可拍摄;关闭仅允许摄影师拍摄 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| -------- | ---------- | ---------------------------------- |
|
||||||
|
| 删除图片 | 无/本人/本部/全部 | 按照图片上传人所在范围进行删除 |
|
||||||
|
| 下载无水印图片 | true/false | 若启用,则下载的图片自动为无水印 |
|
||||||
|
| 允许调整图片顺序 | true/false | 开启后,相应角色人可调图片顺序,保存后以最新一次所调顺序为准 |
|
||||||
|
| 删除普通视频 | 无/本人/本部/全部 | 按照视频方所在的范围进行删除,若无视频方,按上传人范围处理 |
|
||||||
|
| 制作AI视频 | 无/本人/本部/全部 | 按视频上传人/视频方控制可发起制作AI视频的范围 |
|
||||||
|
| 删除AI视频 | 无/本人/本部/全部 | 若房源视频已申请制作AI视频,按AI视频制作人范制是否可删除AI视频 |
|
||||||
|
## 房源委托
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| -------- | ---------- | ------------------------------------------ |
|
||||||
|
| 新增委托 | true/false | 若启用,则可新增委托 |
|
||||||
|
| 续签/违约委托 | 无/本人/本部/全部 | 可按委托方所在范围进行续签/违约委托 |
|
||||||
|
| 再次发起委托 | 无/本人/本部/全部 | 可按委托方所在范围对已作废/审批驳回的委托,进行再次发起(前提:需开启新增委托权限) |
|
||||||
|
| 委托设为交易成功 | 无/本人/本部/全部 | 可按委托方所在范围将委托设为交易成功 |
|
||||||
|
| 标记速销金去向 | 无/本人/本部/全部 | 在速销金支持线上出数/回收时,可按委托方所在范围标记/修改速销金去向 |
|
||||||
|
| 委托列表查看 | 无/本人/本部/全部 | 按照委托方的范围查看委托列表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------- | ---------- | --------------------------- |
|
||||||
|
| 委托作废 | 无/本人/本部/全部 | 按照委托方所在范围控制是否可作废委托 |
|
||||||
|
| 修改违约金 | true/false | 若开启,可修改应收违约金 |
|
||||||
|
| 修改速销金出数去向 | true/false | 在速销金不支持线上出数/收回时,可标记速销金出款和去向 |
|
||||||
|
| 委托列表数据导出 | true/false | 若开启,可导出委托列表数据 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 房源政府核验
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------ | ---------- | --------------------------------- |
|
||||||
|
| 新增核验 | true/false | 若开启,则可新增核验 |
|
||||||
|
| 查看核验 | 无/本人/本部/全部 | 开启后,相关角色可查看相关核验素材,若房源无核验方,则默认均可查看 |
|
||||||
|
| 查看核验附件 | 无/本人/本部/全部 | 开启后,相关角色可查看相关核验文件,若房源无核验方,则默认均可查看 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---- | ---------- | ------------------ |
|
||||||
|
| 作废核验 | 无/本人/本部/全部 | 若开启,可按核验方所在范围将核验作废 |
|
||||||
|
## 房源跟进
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ------------------- | ---------- | --------------------------------------------------------------------- |
|
||||||
|
| 查看房源跟进范围 | 无/本人/本部/全部 | 控制房源详情页的跟进查看范围。注:若跟进人发生部门异动,跟进人在部门异动前写的跟进归属于当时所在部门(跟进人本人依然能查看他异动前的跟进) |
|
||||||
|
| 跟进听录音及查看附件 | 无/本人/本部/全部 | 控制员工查看跟进时的听录音及查看附件范围 |
|
||||||
|
| 查看房源地址不受强制写跟进控制 | true/false | 若启用,查看房源地址不需要强制写跟进 |
|
||||||
|
| 查看业主/联系人号码不受强制写跟进控制 | true/false | 若启用,查看业主/联系人号码不需要强制写跟进 |
|
||||||
|
| 拨打业主/联系人号码不受强制写跟进控制 | true/false | 若启用,拨打业主/联系人号码不需要强制写跟进 |
|
||||||
|
| AI标签消除 | true/false | 若启用,则可以对AI标签操作【不准确并消除】,反馈后AI标签会消除 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------- | ---------- | ----------------------------------- |
|
||||||
|
| 隐藏/开放跟进 | 无/本人/本部/全部 | 按照跟进人所在范围进行隐藏/开放跟进 |
|
||||||
|
| 查看隐藏跟进 | 无/本人/本部/全部 | 按照跟进人所在范围查看被隐藏的跟进 |
|
||||||
|
| 置顶/取消置顶跟进 | 无/本人/本部/全部 | 控制员工可置顶或取消置顶房源未隐藏【写入跟进】的范围,维护人默认不受限 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 房源带看
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| -------- | ---------- | -------------------- |
|
||||||
|
| 查看房源带看数据 | 无/本人/本部/全部 | 按照数据权限范围控制查看房源上的带看记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 房源备件
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---- | ---------- | ------------------ |
|
||||||
|
| 新增附件 | true/false | 若启用,则可新增附件 |
|
||||||
|
| 修改附件 | 无/本人/本部/全部 | 可按附件的上传人所在范围进行修改附件 |
|
||||||
|
| 下载附件 | true/false | 控制附件的上传人是否可以下载附件 |
|
||||||
|
| 查看附件 | 无/本人/本部/全部 | 可按附件的上传人所在范围进行查看附件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 管理权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---- | ---------- | --------------- |
|
||||||
|
| 删除附件 | 无/本人/本部/全部 | 按照附件上传人所在范围进行删除 |
|
||||||
|
## 按属性细分权限范围
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| --------------------- | ------------ | ---------------------------------- |
|
||||||
|
| 激活房源 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以激活房源 |
|
||||||
|
| 属性查看范围 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工查看的房源范围 |
|
||||||
|
| 修改为[暂缓/不售/不租/他售/他租]状态 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以修改为[暂缓/不售/不租/他售/他租]状态 |
|
||||||
|
| 修改为[无效房源]状态 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以修改为[无效房源]状态 |
|
||||||
|
| 调价 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以调价 |
|
||||||
|
| 修改房源属性 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以修改房源属性 |
|
||||||
|
| 设置房源等级 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以将房源等级设置为B、C、D级 |
|
||||||
|
| 修改房源相关信息 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以修改房源相关信息 |
|
||||||
|
| 修改/退还钥匙 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以修改/退还钥匙 |
|
||||||
|
| 写跟进 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以写跟进 |
|
||||||
|
| 拨打电话 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以拨打电话 |
|
||||||
|
| 标记号码为无效 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以标记号码为无效 |
|
||||||
|
| 标记号码为有效 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是都可以标记号码为有效 |
|
||||||
|
| 查看楼栋/单元/楼层/房号 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以查看楼栋/单元/楼层/房号 |
|
||||||
|
| 查看业主/联系人号码 | 公盘/私盘/特盘/封盘 | 按房源属性控制员工是否可以查看业主/联系人号码 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 资料房
|
||||||
|
|
||||||
|
### 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ---------------- | ---------- | ----------------------------- |
|
||||||
|
| 资料房列表查看 | 无/本人/本部/全部 | 按照导入人或归属人所在范围进行查看 |
|
||||||
|
| 导入资料房 | true/false | 若启用,则可导入资料房 |
|
||||||
|
| 已转正资料房查看 | true/false | 若启用,可查看已转正详情 |
|
||||||
|
| 查看号码 | true/false | 若启用,则可查看号码 |
|
||||||
|
| 拨打电话 | true/false | 若启用,则可拨打电话 |
|
||||||
|
| 播放录音 | true/false | 若启用,则可播放录音 |
|
||||||
|
| 删除资料房 | true/false | 若启用,则可删除资料房 |
|
||||||
|
| 编辑资料房 | true/false | 若启用,可编辑资料房 |
|
||||||
|
| 查看统计分析 | true/false | 若启用,可查看统计分析 |
|
||||||
|
| 查看资料房电话次数 | 999 | 每天可查看资料房真实号码的次数,999=不限制,0=不允许 |
|
||||||
|
| 不受资料房拨打/查看号码次数限制 | true/false | 若开启,则拨打/查看号码次数不受权限配置次数限制 |
|
||||||
638
Project/fonrey/PRD/权限管理/权限管理模块PRD.md
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
# PRD: 权限管理模块
|
||||||
|
**状态**: Draft
|
||||||
|
**作者**: 产品经理
|
||||||
|
**最后更新**: 2026-04-24(v1.1 锁定多角色合并规则 = 并集/最宽松)
|
||||||
|
**版本**: 1.1
|
||||||
|
**所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
**关联模块**: 组织人事管理、房源管理、客源管理、系统设置
|
||||||
|
|
||||||
|
## 变更历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 作者 | 变更说明 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| v1.0 | — | 产品经理 | 初稿 |
|
||||||
|
| v1.1 | 2026-04-24 | 产品经理 | 锁定多角色合并规则 = 并集 / 最宽松 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题陈述
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
|
||||||
|
房产经纪公司普遍存在多层级组织结构(总部 → 事业部 → 大区 → 区域 → 门店 → 店组),不同层级员工的业务职责和数据访问边界差异显著。在缺乏系统化权限管控的情况下,经纪公司面临以下核心痛点:
|
||||||
|
|
||||||
|
- **数据越权访问风险**:经纪人可随意查看他人客户资料、房源联系方式,导致客源保护机制形同虚设,内部业务竞争失控
|
||||||
|
- **角色权限配置低效**:每个系统账号独立配置权限,权限变更需逐一操作;人员增加或角色调整时,管理员重复劳动量巨大
|
||||||
|
- **权限与岗位不匹配**:系统权限未与职务/职级联动,新员工默认权限可能超出岗位职责边界,造成数据安全隐患
|
||||||
|
- **个性化权限需求无法满足**:部分员工因岗位特殊性需要在通用角色基础上增加或收窄特定权限,纯角色制度缺乏灵活性
|
||||||
|
- **权限变更缺乏追溯**:权限调整无操作日志,出现数据纠纷时无法追溯是谁、何时、做了什么操作
|
||||||
|
|
||||||
|
### 目标用户
|
||||||
|
|
||||||
|
| 角色 | 描述 | 使用频率 |
|
||||||
|
|------|------|----------|
|
||||||
|
| Tenant Admin(租户管理员) | 负责角色创建、权限配置、人员权限分配及批量操作,是本模块的核心使用者 | 每日 |
|
||||||
|
| 店长 / 区域经理 | 查看下属员工当前权限,按需发起权限变更申请 | 按需 |
|
||||||
|
| Agent(经纪人) | 受权限约束的数据访问者,感知到功能入口的显示/隐藏变化 | 被动感知 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目标与成功指标
|
||||||
|
|
||||||
|
| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 |
|
||||||
|
|------|------|----------|--------|----------|
|
||||||
|
| 提升权限配置效率 | 完成一次人员权限分配耗时 | 约 20 分钟(估算,逐条配置) | < 2 分钟(批量设置角色) | 上线后 60 天 |
|
||||||
|
| 降低越权访问事件 | 经纪人越权查看他人客源/联系方式的投诉工单数 | 待统计 | 减少 80% | 上线后 90 天 |
|
||||||
|
| 提升权限管理透明度 | 管理员查找某员工当前权限配置的操作步骤数 | 待统计 | ≤ 3 步触达 | 上线后 30 天 |
|
||||||
|
| 降低权限异常率 | 「权限与角色不一致」人员数量 | 待统计 | < 5% | 持续监控 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 非目标(本期不做)
|
||||||
|
|
||||||
|
- 不包含细化到行级(Row-level)的数据权限(如仅允许查看特定小区的房源);本期数据范围控制采用**五档模型**:本人 / 本组 / 本门店 / 本区域 / 全公司。注意:并非所有权限项均开放五档,各项的实际可选范围以权限编辑页的下拉配置为准(例如某些权限项仅提供「本人 / 本门店 / 全公司」三个选项)
|
||||||
|
- 不包含权限申请审批工作流(员工自助申请权限需上级审批),本期由管理员直接操作
|
||||||
|
- 不包含操作日志的可视化看板(日志写入,查询能力在后续版本规划)
|
||||||
|
- 不包含 IP 白名单、登录时段等安全策略配置(安全策略模块另行规划)
|
||||||
|
- 不包含移动端 App 独立权限配置(移动端权限为 Web 权限的子集,v2 规划)
|
||||||
|
- 不包含外部合作伙伴(联合门店)账号的跨租户权限体系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 用户故事与验收标准
|
||||||
|
|
||||||
|
> **说明**:以下用户故事按核心业务流程顺序排列,覆盖权限管理模块的两个子模块:「权限管理(人员维度)」和「角色管理(角色维度)」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 1:管理员查看人员权限列表
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 在人员列表中查看全公司所有员工的当前角色与权限状态,**So that** 能快速定位权限异常人员并执行调整。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 页面入口路径:顶部导航「人事」→「组织人事」→「权限管理」,面包屑显示「人事OA / 组织人事 / 权限管理」
|
||||||
|
- [ ] 页面包含两个 Tab:「权限管理」(默认选中)和「角色管理」,Tab 切换无需全页刷新
|
||||||
|
- [ ] 页面右上角提供「角色权限帮助」入口链接
|
||||||
|
- [ ] 列表顶部提供两个快速筛选按钮:「全部员工」和「新房交易列表」
|
||||||
|
- [ ] 支持多条件筛选:姓名/员工号(文本输入)、员工部门(下拉选择)、角色(下拉选择)、职务名称(下拉选择)
|
||||||
|
- [ ] 提供「权限与角色权限不一致」快捷筛选按钮(高亮橙色),点击直接筛选出个人权限已被单独修改、与所属角色权限不一致的人员
|
||||||
|
- [ ] 点击「查询」执行筛选,点击「清空条件」重置所有筛选项
|
||||||
|
- [ ] 支持批量操作:勾选员工复选框后,点击「批量设置角色」按钮执行批量角色变更
|
||||||
|
- [ ] 列表支持「导出」功能,导出当前筛选结果
|
||||||
|
- [ ] 员工列表展示列:复选框、员工姓名、工号、部门、职务、角色、管理范围(带「详情」链接)、操作列
|
||||||
|
- [ ] 操作列包含:「修改权限」「复制角色」「扩充范围」「范围」共 4 个操作项
|
||||||
|
- [ ] 管理范围列显示该员工当前数据可见范围(如「上海豪园店二组」),点击「详情」展开管理范围明细
|
||||||
|
- [ ] 列表支持分页:每页显示条数可切换(20 条/页为默认),支持跳转至指定页
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 2:管理员为员工批量设置角色
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 同时为多名员工批量设置同一角色,**So that** 在员工入职或组织架构调整时能高效完成权限初始化,无需逐一操作。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 在人员列表中勾选至少一名员工后,「批量设置角色」按钮从禁用变为可点击
|
||||||
|
- [ ] 点击「批量设置角色」弹出 Modal 对话框,标题为「批量设置角色」
|
||||||
|
- [ ] Modal 内包含必填字段「角色」,为下拉选择器,展示系统中所有可用角色
|
||||||
|
- [ ] 确认后系统将所选角色应用至所有勾选员工,操作成功后给出 Toast 成功提示
|
||||||
|
- [ ] 若批量操作影响了已有个人自定义权限的员工,系统应给出提醒:「该操作将覆盖所选员工的个人自定义权限,请确认」
|
||||||
|
- [ ] 支持取消操作,取消后返回人员列表,已勾选状态保持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 3:管理员修改个人权限
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 为特定员工在角色权限基础上进行个性化权限调整,**So that** 满足因岗位特殊性而需要区别于通用角色权限配置的场景。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 点击人员列表中某员工操作列的「修改权限」后,进入该员工的权限编辑页
|
||||||
|
- [ ] 页面顶部展示员工信息:员工姓名 - 所属部门,及当前角色(带下拉箭头,支持直接在此页切换角色)
|
||||||
|
- [ ] 页面提供权限名称搜索框,支持关键词快速定位权限项
|
||||||
|
- [ ] 左侧为模块导航树,包含以下一级模块(均可折叠/展开):
|
||||||
|
- 首页
|
||||||
|
- 房源(含子菜单:二手 & 租赁、小区、商圈精耕、举证 & 检核、挂牌分析、房源广场等)
|
||||||
|
- 新房
|
||||||
|
- 客源
|
||||||
|
- 交易
|
||||||
|
- 数据
|
||||||
|
- 营销
|
||||||
|
- 人事OA
|
||||||
|
- 合同
|
||||||
|
- 三网
|
||||||
|
- 系统
|
||||||
|
- 移动端
|
||||||
|
- 智能门店
|
||||||
|
- 在线充值
|
||||||
|
- [ ] 点击左侧模块导航,右侧内容区切换至对应模块的权限配置
|
||||||
|
- [ ] 右侧内容区按功能分组展示权限项,每组包含:分组标题(带开关)、权限项列表
|
||||||
|
- [ ] 每个权限项展示:权限名称、说明(权限作用描述)、当前权限值(下拉选项 / 开关 / 数值输入)、「编辑」操作
|
||||||
|
- [ ] 权限值类型包含:
|
||||||
|
- **开关型**(Toggle):开启 / 关闭
|
||||||
|
- **范围型**(Select):本人 / 本组 / 本门店 / 本区域 / 全公司 等选项(不同权限项的选项范围不同)
|
||||||
|
- **数值型**(Number):如每日最多查看联系人数量(0 = 不限制)
|
||||||
|
- [ ] 模块级总开关(分组标题处的 Toggle)关闭后,该模块下所有权限项均置灰,对应菜单入口隐藏
|
||||||
|
- [ ] 「编辑」操作点击后弹出侧边抽屉(Drawer),展示该权限项的详细说明及当前角色应用人数和应用人员名单
|
||||||
|
- [ ] 修改权限后,若该员工权限与其角色默认权限存在差异,系统在人员列表中标记「权限与角色权限不一致」
|
||||||
|
- [ ] 页面提供「保存」按钮,保存成功后给出成功提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 4:管理员查看并编辑特定权限项(侧边抽屉)
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 在编辑单个权限项时,同时看到该权限当前应用该角色的所有人员名单,**So that** 能了解本次修改的影响范围,再决定是否确认变更。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 点击权限项的「编辑」按钮后,页面右侧滑出 Drawer(不覆盖左侧导航)
|
||||||
|
- [ ] Drawer 顶部展示:角色名称 + 应用人数(如「角色:高级业务员 | 应用人数: 12」),并提供「查看详情」链接
|
||||||
|
- [ ] Drawer 内展示该角色下所有应用人员的姓名和所属部门(格式:姓名-部门名)
|
||||||
|
- [ ] Drawer 中列出本权限项的配置表格:权限名称、说明、当前值(下拉选择),支持在 Drawer 内直接修改
|
||||||
|
- [ ] Drawer 底部提供「保存」和「取消」按钮
|
||||||
|
- [ ] 若权限修改后影响角色所有应用人员,系统提示「权限修改后将影响该角色所有应用人员」
|
||||||
|
- [ ] 保存后 Drawer 关闭,右侧内容区对应权限项显示更新后的值
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 5:管理员查看角色列表
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 在角色管理页查看所有已创建的角色及其基本信息,**So that** 快速了解当前系统的角色体系,并按需编辑或删除。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 点击顶部「角色管理」Tab 切换至角色列表页
|
||||||
|
- [ ] 支持多条件筛选:角色名称(文本输入)、角色类别(下拉,全选)、修改时间(日期范围,开始时间 + 结束时间)
|
||||||
|
- [ ] 点击「查询」执行筛选,点击「清空条件」重置
|
||||||
|
- [ ] 操作区提供:「+ 新增角色」按钮(主按钮,橙色)、「批量删除角色」按钮
|
||||||
|
- [ ] 显示总记录条数(如「① 共 5 条」)
|
||||||
|
- [ ] 角色列表展示列:复选框、角色名称(可排序)、角色类别、应用人数(含「查看」链接)、引用该角色配置、创建时间、修改时间、操作列
|
||||||
|
- [ ] 操作列包含:「编辑」「删除」「修改日志」
|
||||||
|
- [ ] 点击「应用人数」旁的「查看」链接,弹出该角色应用人员名单
|
||||||
|
- [ ] 「引用该角色配置」列显示该角色的权限模板是从哪个已有角色复制/引用的(如「初始劝房」)
|
||||||
|
- [ ] 支持分页,默认 20 条/页
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 6:管理员新增角色
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 新建一个角色并配置其权限,**So that** 为新增岗位或特殊职能定义标准权限模板,便于批量分配给对应员工。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 点击「+ 新增角色」按钮弹出 Modal,标题为「添加角色」
|
||||||
|
- [ ] Modal 内包含两个必填字段:
|
||||||
|
- **角色名称**(文本输入,必填)
|
||||||
|
- **角色类别**(下拉选择,必填):包含「置业顾问」「店管」「总经」等类别选项
|
||||||
|
- [ ] Modal 底部提示:「角色类别影响权限,创建后仅本人创建类别可修改」
|
||||||
|
- [ ] Modal 操作按钮:「下一步:设置权限」(橙色主按钮)、「取消」
|
||||||
|
- [ ] 点击「下一步:设置权限」后,跳转至权限配置详情页,进入角色权限编辑状态
|
||||||
|
- [ ] 角色名称未填写或角色类别未选择时,点击「下一步」显示字段级错误提示,阻止提交
|
||||||
|
- [ ] 创建成功后,新角色出现在角色列表中,创建时间为当前时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 7:管理员配置角色权限
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 在角色权限编辑页为角色精细配置各模块的权限开关和数据范围,**So that** 该角色下所有人员自动继承统一的权限配置,减少重复操作。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 角色权限编辑页顶部展示:角色名称、角色类别、「编辑」按钮(支持在线修改角色基本信息)
|
||||||
|
- [ ] 顶部操作区包含:「导入角色模板」(从已有角色复制权限配置)、「清空」、「保存」按钮
|
||||||
|
- [ ] 左侧模块导航结构与人员权限编辑页一致(首页、房源、客源、交易、数据、营销、人事OA、合同、三网、系统、移动端、智能门店、在线充值)
|
||||||
|
- [ ] 每个模块有「本模块开启」总开关(Toggle),关闭后该模块下所有权限项置灰,菜单入口隐藏
|
||||||
|
- [ ] 权限项按业务分组展示,分组有独立的开关和说明文字
|
||||||
|
- [ ] 各权限项支持的值类型与人员权限编辑页保持一致(开关型 / 范围型 / 数值型)
|
||||||
|
- [ ] 支持「导入角色模板」:选择一个已有角色,将其权限配置复制到当前角色(覆盖当前配置,需二次确认)
|
||||||
|
- [ ] 点击「清空」重置所有权限为默认最小值(需二次确认)
|
||||||
|
- [ ] 点击「保存」保存权限配置,成功后 Toast 提示;应用该角色的所有员工权限实时生效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Story 8:管理员修改角色(切换员工角色)
|
||||||
|
|
||||||
|
**As** Tenant Admin(租户管理员),**I want** 在员工权限编辑页直接切换员工所属角色,**So that** 快速完成角色变更而不需要退出到人员列表再操作。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [ ] 在员工权限编辑页顶部,角色名称旁有下拉箭头,点击展开角色选择器
|
||||||
|
- [ ] 角色选择器展示当前系统中所有可用角色(多选,支持同时分配多个角色)
|
||||||
|
- [ ] 已选择的角色显示为 Tag 形式,可通过 × 取消
|
||||||
|
- [ ] 修改角色后,右侧权限配置区自动刷新至新角色权限设置
|
||||||
|
- [ ] 若新角色权限与员工当前个人自定义权限存在差异,系统提示是否保留个人自定义权限或以角色权限覆盖
|
||||||
|
- [ ] 操作通过「修改角色」弹窗完成,弹窗关闭后权限编辑页显示新角色配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 功能说明
|
||||||
|
|
||||||
|
### 5.1 子模块结构
|
||||||
|
|
||||||
|
权限管理模块分为两个子模块,均位于「人事OA / 组织人事 / 权限管理」路径下:
|
||||||
|
|
||||||
|
| 子模块 | 功能定位 |
|
||||||
|
|--------|----------|
|
||||||
|
| 权限管理(人员视角) | 以员工为维度,查看、分配、个性化调整每个员工的权限 |
|
||||||
|
| 角色管理(角色视角) | 以角色为维度,创建和配置权限模板,供批量分配使用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 权限模型设计
|
||||||
|
|
||||||
|
Fonrey 采用 **RBAC(基于角色的访问控制)+ 个人权限叠加** 的混合权限模型:
|
||||||
|
|
||||||
|
```
|
||||||
|
员工实际权限 = 角色基础权限 + 个人定制权限覆盖
|
||||||
|
```
|
||||||
|
|
||||||
|
- **角色权限**:角色是权限的"模板",相同角色的员工拥有一致的默认权限
|
||||||
|
- **个人权限**:管理员可在角色基础上为特定员工增加或收窄特定权限项,形成个人定制权限
|
||||||
|
- **个人权限优先**:当个人权限与角色权限存在差异时,个人权限值生效
|
||||||
|
- **不一致标记**:系统在人员列表中标记「权限与角色权限不一致」的员工,方便管理员识别并决定是否同步
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 权限管理 - 人员列表
|
||||||
|
|
||||||
|
#### 5.3.1 列表结构
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 员工姓名 | 显示姓名,可作为搜索关键词 |
|
||||||
|
| 工号 | 员工系统工号 |
|
||||||
|
| 部门 | 当前所属部门 |
|
||||||
|
| 职务 | 当前职务 |
|
||||||
|
| 角色 | 当前分配的角色名称 |
|
||||||
|
| 管理范围 | 该员工数据可见范围(如「上海豪园店二组」),点击「详情」查看明细 |
|
||||||
|
| 操作 | 修改权限 / 复制角色 / 扩充范围 / 范围 |
|
||||||
|
|
||||||
|
#### 5.3.2 筛选条件
|
||||||
|
|
||||||
|
| 筛选项 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 搜索 | 文本输入 | 支持姓名、员工号模糊搜索 |
|
||||||
|
| 员工部门 | 下拉 | 选择部门过滤 |
|
||||||
|
| 角色 | 下拉 | 按角色名过滤 |
|
||||||
|
| 职务名称 | 下拉 | 按职务过滤 |
|
||||||
|
| 权限与角色权限不一致 | 快捷筛选按钮 | 一键筛选个人权限已被单独定制的员工 |
|
||||||
|
|
||||||
|
#### 5.3.3 行级操作说明
|
||||||
|
|
||||||
|
| 操作 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 修改权限 | 进入该员工的个人权限编辑页,在角色权限基础上进行个性化调整 |
|
||||||
|
| 复制角色 | 将当前员工的角色(包含个人定制权限)复制给其他员工 |
|
||||||
|
| 扩充范围 | 调整该员工的数据可见范围(如从「本组」扩展到「本门店」) |
|
||||||
|
| 范围 | 查看当前员工的管理范围详情 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 权限管理 - 个人权限编辑页
|
||||||
|
|
||||||
|
#### 5.4.1 模块导航(左侧树形菜单)
|
||||||
|
|
||||||
|
| 模块 | 子菜单(示例) |
|
||||||
|
|------|---------------|
|
||||||
|
| 首页 | 首页 |
|
||||||
|
| 房源 | 二手 & 租赁 / 小区 / 商圈精耕 / 举证 & 检核 / 挂牌分析 / 房源广场 |
|
||||||
|
| 新房 | 新房楼盘管理 / 新房(置业顾问视角)|
|
||||||
|
| 客源 | 客源 |
|
||||||
|
| 交易 | 交易管理 / 二手房售后管理 / 新房售后管理 |
|
||||||
|
| 数据 | 报表管理 / 资料客统计 / 行程量化 / 房客权益查询 |
|
||||||
|
| 营销 | 短视频 / 巧克力(营销工具)|
|
||||||
|
| 人事OA | 组织 / 审批 / 考勤 / 任务 / 内容 |
|
||||||
|
| 合同 | 合同管理 |
|
||||||
|
| 三网 | 三网经纪人后台 / 三网授权管理 |
|
||||||
|
| 系统 | 系统工具 / 业务工具 / 安装与登录授权 |
|
||||||
|
| 移动端 | 移动端 |
|
||||||
|
| 智能门店 | 智慧大屏 / VR 换装 |
|
||||||
|
| 在线充值 | 电话充值 / 增值服务 |
|
||||||
|
|
||||||
|
#### 5.4.2 权限值类型
|
||||||
|
|
||||||
|
| 类型 | 控件形式 | 示例权限项 |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| 开关型 | Toggle(开 / 关) | 今日新上房源是否显示、管理点赞信息和屏蔽点赞 |
|
||||||
|
| 范围型 | 下拉选择 | 查看私客列表(本人 / 本组 / 本门店 / 全公司)|
|
||||||
|
| 数值型 | 数字输入框 | 每日最多查看联系人数量(0 = 不限制)|
|
||||||
|
|
||||||
|
#### 5.4.3 客源模块权限分组(参考截图详细梳理)
|
||||||
|
|
||||||
|
客源模块的权限按以下分组组织:
|
||||||
|
|
||||||
|
**私客基础权限**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 个人私客数量上限 | 数值型 | 999+ = 不限制,0 = 不允许 |
|
||||||
|
| 查看私客房源进出页(非保护客) | 范围型 | 控制查看客源保护的私客房源进出页 |
|
||||||
|
| 查看私客房源进出页(保护客) | 范围型 | 控制已保护客的私客房源进出页 |
|
||||||
|
| 私客转公客 | 范围型 | — |
|
||||||
|
| 查看私客(保护客) | 范围型 | 查看己经纪人名下的保护客 |
|
||||||
|
| 查看私客(合保客) | 范围型 | — |
|
||||||
|
| 编辑私客(保护客) | 范围型 | 编辑本人工员工名下保护的私客信息 |
|
||||||
|
| 设置取消私客保护范围 | 范围型 | 设置取消相关员工的私客的保护范围 |
|
||||||
|
| 私客(非保护)承接客 | 范围型 | 私客(非保护客)承接客的范围 |
|
||||||
|
| 私客(保护客)承接客 | 范围型 | 私客(保护客)承接客的范围 |
|
||||||
|
|
||||||
|
**公客基础权限**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 公客查看范围 | 范围型 | 控制公客查看范围 |
|
||||||
|
| 查看公客详情 | 开关型 | — |
|
||||||
|
| 查看公私特殊客 | 开关型 | 跟公客查看范围和相同,启用后,还可对对查看范围内的公客特殊客,若关闭,无法在公客查看私客 |
|
||||||
|
| 改公客状态 | 开关型 | — |
|
||||||
|
| 编辑公客 | 开关型 | — |
|
||||||
|
| 公客开客资格 | 范围型 | 公客开客资格的范围 |
|
||||||
|
| 公客详情跟进记录查看范围 | 范围型 | 以跟客人为准,控制跟进记录可看到的跟客历史记录 |
|
||||||
|
|
||||||
|
**成交客基础权限**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 查看成交(私客类型) | 范围型 | 控制员工查看的成交情况范围 |
|
||||||
|
| 查看成交(公客类型) | 范围型 | 控制员工查看公客号码的成交来源范围;查看成交权限为本组时,支持查看本组及以上共享层级的成交 |
|
||||||
|
| 查看成交跟进 | 范围型 | — |
|
||||||
|
| 成交客查看范围切换 | 开关型 | 跟成交客查看范围和相同,启用后,还可对对查看范围内的成交客实两次切换,若关闭,无法在成交客查看范围以外切换 |
|
||||||
|
| 查看成交编辑来源人员限来的成交来源字段 | 范围型 | 控制查看成交编辑来源人员填写的成交来源字段 |
|
||||||
|
| 查看成交客备注 | 范围型 | — |
|
||||||
|
| 形成成交客列列表 | 开关型 | 成交客列号列表 |
|
||||||
|
|
||||||
|
**联系人基础权限**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 私客(非保护客)成交查看号码 | 范围型 | 控制查看客源保护的私客、私客的号码范围 |
|
||||||
|
| 成交客查看号码 | 范围型 | 控制查看成交客的私客的号码范围;支持查看本组及以上共享层级的成交号码 |
|
||||||
|
| 私客(保护客)号码 | 范围型 | 控制查看保护客的号码范围 |
|
||||||
|
| 营销短信/成交联系人【联系人名称】最多个数 | 数值型 | — |
|
||||||
|
| 查看【公客人号码】最多个数 | 数值型 | 控制公客查看号码的范围,999+ = 不限制 |
|
||||||
|
| 拨打私客(非保护客)电话 | 范围型 | 控制拨打非保护私客的号码范围 |
|
||||||
|
| 拨打私客(保护客)电话 | 范围型 | 控制拨打保护私客的号码范围 |
|
||||||
|
| 成交客打电话 | 范围型 | 控制拨打成交客的号码范围 |
|
||||||
|
| 公客拨打电话 | 开关型 | 控制拨打公客的号码范围 |
|
||||||
|
| 编辑私客(非保护客)& 成交联系人 | 范围型 | 控制修改保护客、私客的号码信息,有权时对对刷新联系人 |
|
||||||
|
| 编辑私客(保护客)联系人 | 范围型 | 控制修改保护客联系人号码信息,有权时对对刷新联系人 |
|
||||||
|
| 编辑私客(非保护客)& 成交联系人号码 | 范围型 | 控制修改保护客、私客的号码信息 |
|
||||||
|
| 编辑私客(保护客)联系人号码 | 范围型 | — |
|
||||||
|
| 公客联系人号码 | 范围型 | 控制公客联系人号码范围 |
|
||||||
|
|
||||||
|
**管理权限**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 前跑源(查看已跑跑前源) | 开关型 | — |
|
||||||
|
| 不公客查看号码可划打次数限制 | 开关型 | — |
|
||||||
|
| 手动原客户为公客 | 开关型 | — |
|
||||||
|
| 某个范围修改关联员工 | 范围型 | 可修改么么范围内的关联员工,包括添加合作人 |
|
||||||
|
| 批量修改私客 / 公客来源 | 开关型 | — |
|
||||||
|
| 查看客户客人操作日志 | 开关型 | 启用后,可查看某人评语中手号码模板、客户客人不可过... |
|
||||||
|
| 不受到拨号手号码打划打次数限制 | 开关型 | — |
|
||||||
|
| 跑跑跑 | 开关型 | — |
|
||||||
|
| 查看客户号码,超频强制刷回跑跑跑 | 开关型 | 乙乙,可以查看更改不超限制号码,普通产业学刷回 |
|
||||||
|
| 查看备案客 | 开关型 | 查看客户学记录 |
|
||||||
|
| 接管跑跑新客 | 开关型 | 查看化学优化修改客跑跑新接管员工 |
|
||||||
|
| 拆解员工划协办 | 范围型 | 控制拆解员工人员划协办 |
|
||||||
|
| 置换跑跑工跑跑户 | 范围型 | 控制置换跑跑客跑跑户范围 |
|
||||||
|
| 允许合并自己的私跑跑 | 开关型 | 开启后,允许合并入跑客人登录人员合并入本人的私跑跑 |
|
||||||
|
|
||||||
|
**空客**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 空跑跑单中哪些单元号房号查看 | 下拉选择 | 以置业顾问人为准 |
|
||||||
|
| 空跑跑跑单查看范围 | 范围型 | 以置业顾问人为准 |
|
||||||
|
| 空跑跑单中件查看范围 | 范围型 | 以上述人为准 |
|
||||||
|
|
||||||
|
**带看/随行权限**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 带跑跑新增 | 开关型 | — |
|
||||||
|
| 带跑跑单中哪些单元号查看 | 范围型 | 以某某人为准 |
|
||||||
|
| 带跑跑编辑、作废 | 范围型 | — |
|
||||||
|
| 私客/成交跑跑记录查看范围 | 范围型 | 以某某人为准 |
|
||||||
|
| 查看单中中体查看 | 范围型 | 以上述人为准 |
|
||||||
|
| 公客详情跑跑用应用单查看 | 范围型 | 以上述人为准,此时以跑跑员及跑跑项实项目生效 |
|
||||||
|
|
||||||
|
#### 5.4.4 楼盘(小区)模块权限分组
|
||||||
|
|
||||||
|
**楼盘管理**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 楼盘管理查看 | 开关型 | 关闭后,则不显示楼盘管理系统模块 |
|
||||||
|
| 楼盘结构查看 | 开关型 | 开启后,可以查看楼栋-单元-房号数据 |
|
||||||
|
| 新增/批量新增楼盘 | 开关型 | 允许新增楼盘 |
|
||||||
|
| 新增/批量新增楼栋、单元、房号 | 开关型 | 新增楼栋、单元、房号数据 |
|
||||||
|
| 编辑楼盘 | 开关型 | 编辑楼盘 |
|
||||||
|
| 编辑楼栋/单元/房号信息 | 开关型 | 编辑楼栋、单元、房号信息 |
|
||||||
|
| 关联标准库/取关 | 开关型 | 若启用,则可关联至标准楼盘、标准楼栋、单元、房号 |
|
||||||
|
| 删除楼盘 | 开关型 | 删除楼盘 |
|
||||||
|
| 删除楼盘数据(一并删除房源) | 开关型 | 若启用,则可无视是否存在房源,对不同层级及以下的数据全部删除 |
|
||||||
|
| 删除楼栋、单元、房号 | 开关型 | 删除楼栋、单元、房号 |
|
||||||
|
| 合并楼盘 | 开关型 | 若启用,则可合并不同层级楼盘数据(楼栋、单元、房号)|
|
||||||
|
| 移动楼栋/单元/房号数据 | 开关型 | 若启用,则可将A楼盘楼栋单元及以下数据移动至B楼盘;转移房号不能跨小区进行转移 |
|
||||||
|
| 锁定/解锁楼盘 | 开关型 | 操作锁定解锁楼盘 |
|
||||||
|
| 候审房源地址数据查看范围 | 范围型 | 设置员工是否查看部门内其他员工的候审房源地址数据 |
|
||||||
|
| 楼盘挂牌成交数据 | 开关型 | 开启后,显示楼盘挂牌及成交数据信息 |
|
||||||
|
| 司内成交明细及套数 | 开关型 | 开启后,显示公司成交的房源明细信息及成交套数 |
|
||||||
|
| 区域管理 | 开关型 | 若启用,则可对区域商圈进行新增、合并、关联操作 |
|
||||||
|
| 查看销控盘 | 开关型 | 开启后,可在楼盘管理系统-楼盘里,查看销控盘;注意:员工查看销控盘时房源地址是直接可见的,建议只给管理层开启!!!|
|
||||||
|
| 查看销控盘时,只可查看本部门作业范围内的楼盘 | 开关型 | 开启后,只可查看本部门作业范围内的楼盘的销控盘;关闭,则跟作业范围无关,「查看销控盘」权限开启即可查看所有楼盘的销控盘;Tenant Admin(租户管理员)不受限制 |
|
||||||
|
|
||||||
|
**楼盘资料管理**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 楼盘照片 | 开关型 | 开启后,显示楼盘照片列表 |
|
||||||
|
| 管理照片 | 开关型 | 楼盘管理系统-楼盘照片,包含上传照片、设为封面 |
|
||||||
|
| 删除照片 | 开关型 | 允许删除照片 |
|
||||||
|
| 下载照片 | 开关型 | 允许下载照片 |
|
||||||
|
| 楼盘附件 | 开关型 | 开启后,显示楼盘附件模块 |
|
||||||
|
| 管理附件 | 开关型 | 允许上传楼盘附件 |
|
||||||
|
| 下载附件 | 开关型 | 允许下载楼盘附件 |
|
||||||
|
| 删除附件 | 开关型 | 允许删除楼盘附件 |
|
||||||
|
| 周边配套 | 开关型 | 开启后,显示周边配套模块 |
|
||||||
|
| 学校管理列表 | 开关型 | 开启后,显示楼盘管理系统中的学校管理列表 |
|
||||||
|
| 学校管理 | 开关型 | 包含新增、编辑、删除 |
|
||||||
|
|
||||||
|
**楼盘处理**
|
||||||
|
|
||||||
|
| 权限项 | 值类型 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 楼盘反馈列表 | 范围型 | 可查看小区反馈列表的数据范围 |
|
||||||
|
| 楼盘反馈处理 | 开关型 | 包含处理、不予处理操作 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 角色管理
|
||||||
|
|
||||||
|
#### 5.5.1 角色列表
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 角色名称 | 角色唯一名称,支持排序 |
|
||||||
|
| 角色类别 | 如置业顾问、店管、总经等类别,影响权限可选范围 |
|
||||||
|
| 应用人数 | 当前使用该角色的员工数量,点击「查看」查看名单 |
|
||||||
|
| 引用该角色配置 | 该角色权限是基于哪个角色模板创建的 |
|
||||||
|
| 创建时间 | 角色创建时间 |
|
||||||
|
| 修改时间 | 最后一次权限修改时间 |
|
||||||
|
|
||||||
|
#### 5.5.2 已知系统内置角色
|
||||||
|
|
||||||
|
以下角色为系统内置角色,**用户可新增角色,但这些角色不可删除**。
|
||||||
|
|
||||||
|
| 角色名称 |
|
||||||
|
| ---- |
|
||||||
|
| 置业顾问 |
|
||||||
|
| 店管 |
|
||||||
|
| 区管 |
|
||||||
|
| 区总 |
|
||||||
|
| 副总 |
|
||||||
|
| 总经 |
|
||||||
|
| 其他职能 |
|
||||||
|
|
||||||
|
#### 5.5.3 角色类别说明
|
||||||
|
|
||||||
|
角色类别在创建时必选,且创建后**不可随意修改**(仅允许创建者修改),原因是角色类别会直接影响权限的可配置范围(不同类别允许的权限上限不同)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 数据范围(管理范围)说明
|
||||||
|
|
||||||
|
权限管理中「数据范围」是权限系统的核心维度,控制员工可以看到哪个层级的业务数据:
|
||||||
|
|
||||||
|
| 数据范围值 | 说明 |
|
||||||
|
|-----------|------|
|
||||||
|
| 本人 | 仅查看/操作自己名下的数据 |
|
||||||
|
| 本组 | 查看/操作所在店组的数据 |
|
||||||
|
| 本门店 | 查看/操作所在门店的所有数据 |
|
||||||
|
| 本区域 | 查看/操作所在区域范围内的数据 |
|
||||||
|
| 全公司 | 查看/操作公司所有数据(管理层权限)|
|
||||||
|
| 无 | 无权查看/操作 |
|
||||||
|
|
||||||
|
> 注:不同权限项的可选范围不同,如某权限项只有「本人 / 本门店 / 全公司」三个选项,另一权限项可能有完整的五档选项。具体可选范围以权限编辑页的下拉选项为准。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 技术考量
|
||||||
|
|
||||||
|
### 依赖关系
|
||||||
|
|
||||||
|
| 依赖系统 / 模块 | 用途 | 风险等级 |
|
||||||
|
|----------------|------|----------|
|
||||||
|
| 组织人事管理(org 模块) | 员工 / 部门数据读取,权限与员工身份绑定 | 高 |
|
||||||
|
| django-tenants | 权限数据必须严格按租户 Schema 隔离,严禁跨租户查询 | 高 |
|
||||||
|
| Redis 缓存 | 员工权限频繁读取,建议缓存员工权限快照,更新时主动失效 | 中 |
|
||||||
|
|
||||||
|
### 已知风险
|
||||||
|
|
||||||
|
| 风险 | 可能性 | 影响 | 缓解策略 |
|
||||||
|
|------|--------|------|----------|
|
||||||
|
| 权限变更实时生效的一致性问题 | 中 | 高 | 角色/个人权限变更后主动清除 Redis 中该员工权限缓存,强制下次请求重新加载 |
|
||||||
|
| 权限项数量巨大(14+ 模块,数百个权限项)导致编辑页性能问题 | 中 | 中 | 左侧导航按模块懒加载权限数据,避免一次性加载全部权限项 |
|
||||||
|
| 多角色合并规则(已锁定 v1.1) | - | - | **规则**:同一员工多角色取**并集(最宽松原则)**。BOOLEAN→OR;SCOPE→MAX(self<store<zone<area<region<division<company);INTEGER(限额类)→MAX,`0` 视为无上限。详见 `DATA_MODEL_PERMISSION.md` |
|
||||||
|
| 角色删除影响已分配员工 | 低 | 高 | 删除角色前校验是否有员工仍使用该角色,有则阻止删除并提示转移员工角色 |
|
||||||
|
|
||||||
|
### 待确认开放问题
|
||||||
|
|
||||||
|
- [x] ~~**多角色权限合并规则**:当员工被分配了多个角色时,权限取并集(最宽松)还是交集(最严格)?~~ **已确认(v1.1,2026-04-24)**:取**并集/最宽松**。BOOLEAN=OR,SCOPE=MAX,INTEGER=MAX(`0`=∞)。权威实现见 `DATA_MODEL_PERMISSION.md` 附录 E `PermissionChecker` 伪代码。
|
||||||
|
- [ ] **个人权限与角色权限冲突优先级**:个人定制权限是否始终覆盖角色权限,还是支持「继承角色 + 仅扩展」模式?— Owner: 产品负责人 — Deadline: 开发启动前
|
||||||
|
- [ ] **权限操作日志**:是否需要记录管理员对权限的变更记录(谁、何时、将哪个权限从什么改成什么)?截图中「修改日志」入口存在,需确认日志颗粒度 — Owner: 产品负责人 — Deadline: 开发启动前
|
||||||
|
- [ ] **角色类别的完整枚举值**:当前已知「置业顾问 / 店管 / 总经」,需确认完整的角色类别列表及各类别允许的权限上限 — Owner: 产品负责人 — Deadline: 开发启动前
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 上线计划
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 受众 | 通过标准 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 内部 Alpha | TBD | 研发 + 产品团队 | 角色 CRUD 流程通畅,权限编辑保存无误 |
|
||||||
|
| 封闭 Beta | TBD | 1-2 家试点门店Tenant Admin(租户管理员) | 批量设置角色 / 个人权限修改功能可用,无 P0 Bug |
|
||||||
|
| 正式上线 | TBD | 全部租户Tenant Admin(租户管理员) | 权限变更实时生效,错误率 < 0.5%,Tenant Admin(租户管理员) CSAT ≥ 4/5 |
|
||||||
|
|
||||||
|
**回滚标准**:若权限写入后错误率 > 2%,或出现跨租户数据泄漏问题,立即回滚并通知所有租户管理员。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 附录
|
||||||
|
|
||||||
|
### 8.1 截图参考索引
|
||||||
|
|
||||||
|
| 截图文件 | 完整路径 | 对应功能 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `权限管理-人员列表.png` | `Project/fonrey/screenshots/权限管理/权限管理-人员列表.png` | 5.3 权限管理人员列表 |
|
||||||
|
| `权限管理-修改个人权限-客源.png` | `Project/fonrey/screenshots/权限管理/权限管理-修改个人权限-客源.png` | 5.4 个人权限编辑 - 客源模块 |
|
||||||
|
| `权限管理-人员编辑特定权限.png` | `Project/fonrey/screenshots/权限管理/权限管理-人员编辑特定权限.png` | Story 4 - 侧边 Drawer 编辑单个权限项 |
|
||||||
|
| `权限管理-批量设置角色.png` | `Project/fonrey/screenshots/权限管理/权限管理-批量设置角色.png` | Story 2 - 批量设置角色 Modal |
|
||||||
|
| `角色管理-角色列表.png` | `Project/fonrey/screenshots/权限管理/角色管理-角色列表.png` | 5.5.1 角色列表 |
|
||||||
|
| `角色管理-添加角色1.png` | `Project/fonrey/screenshots/权限管理/原始图片/角色管理-添加角色1.png` | Story 6 - 新增角色 Modal |
|
||||||
|
| `角色管理-修改角色.png` | `Project/fonrey/screenshots/权限管理/原始图片/角色管理-修改角色.png` | Story 8 - 修改角色(切换员工角色)|
|
||||||
|
| `权限-房源-小区.png` | `Project/fonrey/screenshots/权限管理/权限-房源-小区.png` | 5.4.4 楼盘(小区)模块权限分组 |
|
||||||
|
| `权限-客源-客源.png` | `Project/fonrey/screenshots/权限管理/权限-客源-客源.png` | 5.4.3 客源模块权限分组(角色编辑视角)|
|
||||||
|
| `权限-房源-挂牌分析.png` | `Project/fonrey/screenshots/权限管理/权限-房源-挂牌分析.png` | 5.4 房源 - 挂牌分析模块权限 |
|
||||||
|
| `权限-房源-商圈精耕.png` | `Project/fonrey/screenshots/权限管理/权限-房源-商圈精耕.png` | 5.4 房源 - 商圈精耕模块权限 |
|
||||||
|
| `权限-房源-举证检核.png` | `Project/fonrey/screenshots/权限管理/权限-房源-举证检核.png` | 5.4 房源 - 举证检核模块权限 |
|
||||||
|
| `权限-客源-客源-table.png` | `Project/fonrey/screenshots/权限管理/权限-客源-客源-table.png` | 5.4.3 客源权限表格视图 |
|
||||||
|
| `权限-合同-合同管理.png` | `Project/fonrey/screenshots/权限管理/权限-合同-合同管理.png` | 5.4 合同管理模块权限 |
|
||||||
|
| `权限-人事QA-考勤.png` | `Project/fonrey/screenshots/权限管理/权限-人事QA-考勤.png` | 5.4 人事OA - 考勤权限 |
|
||||||
|
| `权限-人事QA-组织.png` | `Project/fonrey/screenshots/权限管理/权限-人事QA-组织.png` | 5.4 人事OA - 组织权限 |
|
||||||
|
| `权限-人事QA-审批.png` | `Project/fonrey/screenshots/权限管理/权限-人事QA-审批.png` | 5.4 人事OA - 审批权限 |
|
||||||
|
| `权限-人事QA-任务.png` | `Project/fonrey/screenshots/权限管理/权限-人事QA-任务.png` | 5.4 人事OA - 任务权限 |
|
||||||
|
| `权限-人事QA-内容.png` | `Project/fonrey/screenshots/权限管理/权限-人事QA-内容.png` | 5.4 人事OA - 内容权限 |
|
||||||
|
| `权限-交易-交易管理.jpg` | `Project/fonrey/screenshots/权限管理/权限-交易-交易管理.jpg` | 5.4 交易 - 交易管理权限 |
|
||||||
|
| `权限-交易-二手房售后管理.png` | `Project/fonrey/screenshots/权限管理/权限-交易-二手房售后管理.png` | 5.4 交易 - 二手房售后管理权限 |
|
||||||
|
| `权限-交易-新房售后管理.png` | `Project/fonrey/screenshots/权限管理/权限-交易-新房售后管理.png` | 5.4 交易 - 新房售后管理权限 |
|
||||||
|
| `权限-三网-三网经纪人后台.png` | `Project/fonrey/screenshots/权限管理/权限-三网-三网经纪人后台.png` | 5.4 三网 - 三网经纪人后台权限 |
|
||||||
|
|
||||||
|
### 8.2 权限模块完整导航结构
|
||||||
|
|
||||||
|
基于截图梳理的权限配置模块树(以角色编辑页左侧导航为准):
|
||||||
|
|
||||||
|
```
|
||||||
|
权限配置
|
||||||
|
├── 首页
|
||||||
|
├── 房源
|
||||||
|
│ ├── 二手 & 租赁
|
||||||
|
│ ├── 小区(楼盘管理)
|
||||||
|
│ ├── 商圈精耕
|
||||||
|
│ ├── 举证 & 检核
|
||||||
|
│ ├── 挂牌分析
|
||||||
|
│ └── 房源广场
|
||||||
|
├── 新房
|
||||||
|
│ ├── 新房
|
||||||
|
├── 客源
|
||||||
|
│ ├── 客源
|
||||||
|
├── 交易
|
||||||
|
│ ├── 交易管理
|
||||||
|
│ ├── 二手房售后管理
|
||||||
|
│ └── 新房售后管理
|
||||||
|
├── 数据
|
||||||
|
│ ├── 报表管理
|
||||||
|
│ ├── 资料客统计
|
||||||
|
│ ├── 行程量化
|
||||||
|
│ └── 房客权益查询
|
||||||
|
├── 营销
|
||||||
|
│ ├── 短视频
|
||||||
|
│ └── 巧克力
|
||||||
|
├── 人事OA
|
||||||
|
│ ├── 组织
|
||||||
|
│ ├── 审批
|
||||||
|
│ ├── 考勤
|
||||||
|
│ ├── 任务
|
||||||
|
│ └── 内容
|
||||||
|
├── 合同
|
||||||
|
│ └── 合同管理
|
||||||
|
├── 三网
|
||||||
|
│ ├── 三网经纪人后台
|
||||||
|
│ └── 三网授权管理
|
||||||
|
├── 系统
|
||||||
|
│ ├── 系统工具
|
||||||
|
│ ├── 业务工具
|
||||||
|
│ └── 安装与登录授权
|
||||||
|
├── 移动端
|
||||||
|
│ ├── 移动端
|
||||||
|
├── 智能门店
|
||||||
|
│ ├── 智慧大屏
|
||||||
|
│ └── VR 换装
|
||||||
|
└── 在线充值
|
||||||
|
├── 电话充值
|
||||||
|
└── 增值服务
|
||||||
|
```
|
||||||
32
Project/fonrey/PRD/权限管理/组织人事-组织.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
| 管理权限 | 🔴(开启) | 说明 |
|
||||||
|
| ---------------- | ---------- | --------------------------------------------------------------------------------------------------- |
|
||||||
|
| 组织结构查看 | 无/本人/本部/全部 | 控制组织结构页面的部门/员工查看范围 |
|
||||||
|
| 部门查看 | true/false | 控制是否可以查看部门信息 |
|
||||||
|
| 部门维护 | true/false | 控制是否可以对部门进行编辑操作 |
|
||||||
|
| 员工查看 | true/false | 控制是否可以查看员工详情 |
|
||||||
|
| 员工维护 | true/false | 控制是否可以查看员工详情、进行员工异动、批量设置员工上级 |
|
||||||
|
| 员工详情编辑 | true/false | 控制是否能编辑员工信息、新增奖惩记录、编辑账号信息、上传员工相关资料、复职员工,该权限控制的功能点需要与「员工查看和维护」权限共同开启才能正常使用 |
|
||||||
|
| 员工账号冻结/解冻 | true/false | 控制是否可以冻结/解冻员工账号 |
|
||||||
|
| 批量导入员工 | true/false | 控制是否可以批量导入员工 |
|
||||||
|
| 导出员工 | true/false | 控制是否可以导出员工 |
|
||||||
|
| 员工权限查看 | true/false | |
|
||||||
|
| 员工权限设置 | true/false | 控制是否能够编辑员工权限 |
|
||||||
|
| 权限管理页面导出 | true/false | 开启后可在权限管理页面导出员工角色及管理范围数据 |
|
||||||
|
| 营销号绑定 | true/false | |
|
||||||
|
| 职务维护 | true/false | 控制是否能够新增/编辑/删除/合并员工职务 |
|
||||||
|
| 角色维护 | true/false | 控制是否展示角色管理页面 |
|
||||||
|
| 门店列表查看 | true/false | 若启用,则可查看门店列表 |
|
||||||
|
| 门店列表导出 | true/false | 若启用,则可导出门店列表 |
|
||||||
|
| 门店分布图查看 | true/false | 若启用,则可查看门店分布图列表 |
|
||||||
|
| 员工通讯录查看 | 无/本人/本部/全部 | 控制是否能够查看员工通讯录 |
|
||||||
|
| 员工通讯录电话查看 | 999 | 每天可查看员工通讯录电话查看次数,999=不限制,0=不允许 |
|
||||||
|
| 员工通讯录电话拨打 | 999 | 每天可通过巧房小号拨打员工号码次数,999=不限制,0=不允许 |
|
||||||
|
| 转移业务归属功能 | 无/本人/本部/全部 | 控制转出/转入人的可选范围 |
|
||||||
|
| 清空非有效门店的钥匙委托实勘资源 | true/false | 若开启,则可操作清空所有非有效门店下员工的钥匙委托实勘资源(无需在权限里配置扩充部门权限/加盟架构,所有状态符合的门店均可操作,请谨慎开启)。请注意,此权限需「转移业主归属功能」为本部或全部,才生效 |
|
||||||
|
| 绑定短信/绑定链接 | true/false | 控制员工是否能进行下发绑定短信/绑定链接操作 |
|
||||||
|
| 人工通过身份验证 | true/false | 协助员工完成PC/APP的登录实名校验。 |
|
||||||
|
| 离职申请范围 | 无/本人/本部/全部 | 控制员工离职申请范围 |
|
||||||
|
| 入职邀请 | true/false | 控制员工是否可以生成入职邀请链接/二维码 |
|
||||||
|
| 员工入职黑名单 | true/false | |
|
||||||
344
Project/fonrey/PRD/权限管理/角色权限矩阵.md
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# 角色权限矩阵 — Fonrey MVP
|
||||||
|
|
||||||
|
**版本**: v1.0
|
||||||
|
**更新**: 2026-04-30
|
||||||
|
**依据**: `PERMISSION_SEED_MVP_BATCH1.md`(154 条)× 7 个系统内置角色
|
||||||
|
**状态**: Draft — 待产品 / 业务确认
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 角色说明
|
||||||
|
|
||||||
|
| 角色 | 定位 | 典型层级 |
|
||||||
|
| -------- | ---------------------------- | ------------------ |
|
||||||
|
| 置业顾问 | 一线销售,操作个人名下数据 | 门店员工 |
|
||||||
|
| 店管 | 门店管理,可查看本店数据 | 门店负责人 |
|
||||||
|
| 区管 | 区域管理,可查看本区数据 | 区域负责人 |
|
||||||
|
| 区总 | 区域总负责,更广的操作权限 | 大区总监 |
|
||||||
|
| 副总 | 公司副总,全公司数据可见 | 副总经理 |
|
||||||
|
| 总经 | 最高管理层,全量权限 | 总经理 / 超管角色 |
|
||||||
|
| 其他职能 | 行政、HR 等非业务岗,受限访问 | 职能部门人员 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 取值说明
|
||||||
|
|
||||||
|
| 类型 | 可选值 | 含义 |
|
||||||
|
| ------- | ---------------- | ----------------------------------- |
|
||||||
|
| boolean | `✓` / `✗` | 开启 / 关闭 |
|
||||||
|
| scope | `—` / `本人` / `本部` / `全部` | 无权限 / 本人 / 本部门及以下 / 全公司 |
|
||||||
|
| integer | `0` / 数字 / `∞` | 不允许 / 上限次数 / 不限制 |
|
||||||
|
|
||||||
|
> **scope 继承原则**:「本部」= 本门店及所辖组;「全部」= 全公司。
|
||||||
|
> 区管/区总的「本部」含义对应其管辖的门店范围,不限于单一门店。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、房源模块(property)
|
||||||
|
|
||||||
|
### 1.1 房源基础(property.listing)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 新增房源 | `property.listing.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 维护房源查看范围 | `property.listing.view_scope` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 公盘查看 | `property.listing.view_public` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 私盘查看 | `property.listing.view_private` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 将房源改为公盘 | `property.listing.set_public` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 将房源改为私盘 | `property.listing.set_private` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 将房源改为封盘 | `property.listing.set_locked` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 将房源改为特盘 | `property.listing.set_special` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除房源 | `property.listing.delete` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 恢复已删除房源 | `property.listing.restore` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 房源列表数据导出 | `property.listing.export` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 修改房屋介绍信息 | `property.listing.edit_description` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 成交房源列表及价格信息 | `property.listing.view_deal` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 价格解读 | `property.listing.price_read` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看房源挂牌历史 | `property.listing.view_history` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看同业主其他房源 | `property.listing.view_owner_others` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 修改房源保护设置 | `property.listing.set_protected` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看保护期内房源 | `property.listing.view_protected` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 修改相关方范围 | `property.listing.change_keeper` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 重复房源合并 | `property.listing.merge_duplicate` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 修改为我售/我租状态 | `property.listing.status_sold` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 将房源等级设为A | `property.listing.grade_set_a` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 将房源等级设为E | `property.listing.grade_set_e` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 1.2 业主/联系人与号码(property.contact)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看业主/联系人号码 | `property.contact.view_phone` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 每日查看号码次数上限 | `property.contact.view_phone_limit` | 20 | ∞ | ∞ | ∞ | ∞ | ∞ | 0 |
|
||||||
|
| 新增业主/联系人 | `property.contact.add_contact` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 修改业主核心信息 | `property.contact.edit_core` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 修改业主非核心信息 | `property.contact.edit_basic` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 删除业主/联系人 | `property.contact.delete_contact` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看产证信息 | `property.contact.view_cert` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看业主联系人操作日志 | `property.contact.view_operation_log` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 1.3 房源地址(property.address)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看楼栋/单元/楼层/房号 | `property.address.view_detail` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 每日查看地址次数上限 | `property.address.view_limit` | 10 | ∞ | ∞ | ∞ | ∞ | ∞ | 0 |
|
||||||
|
| 修改楼栋/单元/楼层/房号 | `property.address.edit` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
### 1.4 房源钥匙(property.key)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 新增钥匙 | `property.key.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 修改钥匙 | `property.key.edit` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 退还钥匙 | `property.key.return` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看钥匙密码 | `property.key.view_password` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看钥匙编号 | `property.key.view_number` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 钥匙借出 | `property.key.borrow` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 钥匙归还 | `property.key.give_back` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 删除钥匙 | `property.key.delete` | — | 本人 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 钥匙列表数据导出 | `property.key.export` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 1.5 房源实勘(property.survey)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 新增实勘图片 | `property.survey.create_photo` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 下载图片 | `property.survey.download_photo` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除图片 | `property.survey.delete_photo` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 新增实勘 | `property.survey.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看实勘 | `property.survey.view` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 上传视频 | `property.survey.upload_video` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 下载视频 | `property.survey.download_video` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 播放视频 | `property.survey.play_video` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 1.6 房源委托(property.mandate)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 新增委托 | `property.mandate.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 续签/违约委托 | `property.mandate.renew` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 委托列表查看 | `property.mandate.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 委托作废 | `property.mandate.revoke` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 委托列表数据导出 | `property.mandate.export` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 1.7 房源跟进(property.follow)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看房源跟进范围 | `property.follow.view_scope` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 隐藏/开放跟进 | `property.follow.hide` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看隐藏跟进 | `property.follow.view_hidden` | — | 本人 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 置顶/取消置顶跟进 | `property.follow.pin` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
### 1.8 房源附件(property.attachment)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 新增附件 | `property.attachment.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看附件 | `property.attachment.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 修改附件 | `property.attachment.edit` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 下载附件 | `property.attachment.download` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除附件 | `property.attachment.delete` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
### 1.9 房源带看(property.showing)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看房源带看数据 | `property.showing.view_scope` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、客源模块(client)
|
||||||
|
|
||||||
|
### 2.1 私客(client.private)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 新增私客 | `client.private.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看私客(非保护客) | `client.private.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看私客(保护客) | `client.private.view_protected` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 编辑私客(非保护客) | `client.private.edit` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 编辑私客(保护客) | `client.private.edit_protected` | 本人 | 本人 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 设置/取消保护客 | `client.private.set_protected` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 私客转公客 | `client.private.to_public` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 私客列表导出 | `client.private.export` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 2.2 公客(client.public)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 公客查看范围 | `client.public.view` | — | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 公客转私客 | `client.public.to_private` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 编辑公客 | `client.public.edit` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 改公客状态 | `client.public.change_status` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 2.3 成交客(client.deal)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看成交客(私客类型) | `client.deal.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看成交客(公客类型) | `client.deal.view_public` | — | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 成交客再次租/购 | `client.deal.re_transaction` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 导出成交客列表 | `client.deal.export` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 2.4 联系人号码(client.contact)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看私客/成交客号码 | `client.contact.view_phone_private` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看保护客号码 | `client.contact.view_phone_protected` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看公客号码 | `client.contact.view_phone_public` | — | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 每日查看联系人号码次数上限 | `client.contact.view_phone_limit` | 20 | ∞ | ∞ | ∞ | ∞ | ∞ | 0 |
|
||||||
|
| 编辑私客/成交客联系人 | `client.contact.edit_contact` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 编辑私客/成交客联系人号码 | `client.contact.edit_phone` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
### 2.5 客源管理(client.mgmt)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 删除客源 | `client.mgmt.delete` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 手动客源转为成交客 | `client.mgmt.to_deal` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 单个客源修改相关员工 | `client.mgmt.change_staff` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 批量客源修改相关员工 | `client.mgmt.batch_change_staff` | — | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 查看客户/联系人操作日志 | `client.mgmt.view_operation_log` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 允许合并自己的私客 | `client.mgmt.merge_private` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 2.6 带看/预约(client.showing)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 带看/预约新增 | `client.showing.create` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 私客/成交客详情页带看单查看 | `client.showing.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 带看/预约编辑、作废 | `client.showing.edit` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
### 2.7 资料客(client.archive)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看资料客 | `client.archive.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 导入资料客 | `client.archive.import` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看资料客号码 | `client.archive.view_phone` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除资料客 | `client.archive.delete` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 查看资料客操作日志 | `client.archive.view_log` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、首页模块(home)
|
||||||
|
|
||||||
|
### 3.1 首页看板(home.dashboard)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 查看首页版本 | `home.dashboard.view_version` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 个人排行榜权限 | `home.dashboard.personal_rank` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 部门排行榜权限 | `home.dashboard.dept_rank` | — | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 管理点赞信息和屏蔽点赞 | `home.dashboard.manage_praise` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、楼盘/小区模块(complex)
|
||||||
|
|
||||||
|
### 4.1 楼盘管理(complex.*)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 楼盘管理查看 | `complex.view` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 楼盘结构查看 | `complex.view_structure` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 新增楼盘 | `complex.create` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 新增楼栋/单元/房号 | `complex.create_unit` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 编辑楼盘 | `complex.edit` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 编辑楼栋/单元/房号 | `complex.edit_unit` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除楼盘 | `complex.delete` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除楼栋/单元/房号 | `complex.delete_unit` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除楼盘数据(含房源) | `complex.delete_with_property` | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 合并楼盘 | `complex.merge` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 移动楼栋/单元/房号数据 | `complex.move_unit` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 锁定/解锁楼盘 | `complex.lock` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 楼盘挂牌成交数据 | `complex.view_deal` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 司内成交明细及套数 | `complex.view_deal_detail` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 楼街房源地址数据查看范围 | `complex.view_address_scope` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 区域管理 | `complex.region_manage` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
### 4.2 楼盘资料(complex.material)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 楼盘照片 | `complex.material.view_photo` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 管理照片 | `complex.material.manage_photo` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 删除照片 | `complex.material.delete_photo` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 下载照片 | `complex.material.download_photo` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 楼盘附件 | `complex.material.view_attachment` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 管理附件 | `complex.material.manage_attachment` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 下载附件 | `complex.material.download_attachment` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| 删除附件 | `complex.material.delete_attachment` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 周边配套 | `complex.material.view_surrounding` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
### 4.3 楼盘反馈(complex.feedback)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 楼盘反馈列表 | `complex.feedback.view` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 楼盘反馈处理 | `complex.feedback.handle` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、组织人事-组织模块(org)
|
||||||
|
|
||||||
|
### 5.1 组织管理(org.*)
|
||||||
|
|
||||||
|
| 权限项 | permission_code | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ------ | --------------- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| 组织结构查看 | `org.view_structure` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | 本人 |
|
||||||
|
| 部门查看 | `org.view_dept` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 部门维护 | `org.edit_dept` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工查看 | `org.view_staff` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工维护 | `org.edit_staff` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工详情编辑 | `org.edit_staff_detail` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工账号冻结/解冻 | `org.freeze_account` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 批量导入员工 | `org.import_staff` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 导出员工 | `org.export_staff` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工权限查看 | `org.view_permission` | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工权限设置 | `org.edit_permission` | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 权限管理页面导出 | `org.export_permission` | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 职务维护 | `org.edit_position` | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 角色维护 | `org.edit_role` | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 门店列表查看 | `org.view_store_list` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 门店列表导出 | `org.export_store_list` | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 员工通讯录查看 | `org.view_contact_book` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | 本人 |
|
||||||
|
| 转移业务归属 | `org.transfer_business` | — | 本部 | 本部 | 全部 | 全部 | 全部 | — |
|
||||||
|
| 离职申请范围 | `org.resign_apply` | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | 本人 |
|
||||||
|
| 入职邀请 | `org.invite_onboard` | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||||
|
| 每日查看通讯录电话次数上限 | `org.view_contact_phone_limit` | 5 | ∞ | ∞ | ∞ | ∞ | ∞ | 5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、横向对比:各角色权限宽度一览
|
||||||
|
|
||||||
|
> 快速识别各角色的整体权限层级,便于向业务方解释。
|
||||||
|
|
||||||
|
| 维度 | 置业顾问 | 店管 | 区管 | 区总 | 副总 | 总经 | 其他职能 |
|
||||||
|
| ---- | :------: | :--: | :--: | :--: | :--: | :--: | :------: |
|
||||||
|
| **数据范围(scope)** | 本人 | 本部 | 本部 | 全部 | 全部 | 全部 | 无/本人 |
|
||||||
|
| **房源操作** | 基础增查改 | + 删/改状态/导出 | 同店管 | + 跨区操作 | 同区总 | 全量 | 无 |
|
||||||
|
| **客源查看** | 个人私客 | 本店全量 | 本区全量 | 全公司 | 全公司 | 全公司 | 无 |
|
||||||
|
| **号码查看上限/日** | 20 次 | 不限 | 不限 | 不限 | 不限 | 不限 | 0 |
|
||||||
|
| **组织人事操作** | 无 | 查看/导出 | + 维护/冻结 | + 权限管理/角色 | 同区总 | 全量 | 无 |
|
||||||
|
| **楼盘管理** | 只读 | + 增改删(单元) | + 删楼盘/合并/区域 | + 删楼盘含房源 | 同区总 | 全量 | 只读 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、Open Questions(待确认)
|
||||||
|
|
||||||
|
| # | 问题 | 涉及权限 | 影响角色 | 当前保守默认 | 截止确认日期 |
|
||||||
|
| - | ---- | -------- | -------- | ------------ | ------------ |
|
||||||
|
| Q1 | 置业顾问是否可查看**公盘**?部分公司公盘仅店管以上可见 | `property.listing.view_public` | 置业顾问 | ✓(开放) | — |
|
||||||
|
| Q2 | 置业顾问**每日查看号码次数**(房源/客源),业务方确认上限值 | `property.contact.view_phone_limit` / `client.contact.view_phone_limit` | 置业顾问 | 20 次 | — |
|
||||||
|
| Q3 | 店管是否可**编辑保护客**(`edit_protected`)?设为「本人」还是「本部」 | `client.private.edit_protected` | 店管 | 本人(保守) | — |
|
||||||
|
| Q4 | **权限管理(`org.edit_permission`)** 是否从区总才开放,还是区管也可配置本店员工权限 | `org.view_permission` / `org.edit_permission` | 区管 | ✗(不开放) | — |
|
||||||
|
| Q5 | 其他职能是否需要访问**楼盘模块**(查看楼盘/楼盘资料)? | `complex.view` / `complex.material.*` | 其他职能 | ✓(只读) | — |
|
||||||
|
| Q6 | **副总与总经权限是否完全相同**?还是副总有哪些项目需要受限 | 全量 scope 类 | 副总 | 同总经 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本矩阵基于 `PERMISSION_SEED_MVP_BATCH1.md` 154 条权限定义生成。确认后将同步更新 `PERMISSION_SEED_MVP_BATCH1.md` 内置角色 role_permissions 分配策略。*
|
||||||
13
Project/fonrey/PRD/权限管理/首页.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
**首页** 开启才可使用相关功能;关闭后会去失相关系统所有权限,并隐藏对应菜单入口。 本模块开启 ●
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基础权限 (true/false)
|
||||||
|
|
||||||
|
| 权限项目 | 设置值 | 说明 |
|
||||||
|
| ----------- | --------------------------- | ---------------------------------------------------------------- |
|
||||||
|
| 查看首页版本 | 无/置业顾问/店管/区管/区总/副总/总经理/职能人员 | 控制员工查看的首页版本,其中置业顾问只能查看本人业务数据,店营/区营/区总/副总能查看本部业务数据,总经理能查看全公司业务数据。 |
|
||||||
|
| 今日新上房源 | true/false | 控制移动端是否显示 |
|
||||||
|
| 个人排行榜权限 | 无/本人/本部/全部 | 控制个人排行榜可见数据范围 |
|
||||||
|
| 部门排行榜权限 | 无/本人/本部/全部 | 控制部门排行榜可见数据范围 |
|
||||||
|
| 管理点赞信息和屏蔽点赞 | true/false | 开启后,有权删除首页点赞墙的内容和禁止员工发布点赞。 |
|
||||||
309
Project/fonrey/PRD/登录管理/fonrey-login-forget-pwd-diagram.xml
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<mxfile host="app.diagrams.net" modified="2026-04-25T00:00:00.000Z" agent="OpenCode" version="21.0.0">
|
||||||
|
<!-- ============================================================
|
||||||
|
5.4.1 找回用户名流程
|
||||||
|
============================================================ -->
|
||||||
|
<diagram id="recover-username" name="5.4.1 找回用户名流程">
|
||||||
|
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1"
|
||||||
|
connect="1" arrows="1" fold="1" page="1" pageScale="1"
|
||||||
|
pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<!-- Title -->
|
||||||
|
<mxCell id="t1" value="5.4.1 找回用户名流程" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="175" y="20" width="500" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Start -->
|
||||||
|
<mxCell id="u1" value="用户点击「忘记用户名」" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="80" width="300" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Decision: Is email format valid? -->
|
||||||
|
<mxCell id="u2" value="展示「找回用户名」页面
|
||||||
|
(邮箱输入框 + 发送按钮)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="250" y="170" width="350" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- User inputs email -->
|
||||||
|
<mxCell id="u3" value="用户输入邮箱并点击「发送」" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="250" y="260" width="350" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Decision: Email format valid? -->
|
||||||
|
<mxCell id="u4" value="邮箱格式校验通过?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="350" width="300" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Format invalid -->
|
||||||
|
<mxCell id="u5" value="提示「请输入有效的邮箱地址」" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="620" y="360" width="220" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Backend query -->
|
||||||
|
<mxCell id="u6" value="服务端查询邮箱是否绑定账号
|
||||||
|
(不向前端返回查询结果)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="250" y="470" width="350" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Unified response to frontend -->
|
||||||
|
<mxCell id="u7" value="统一响应前端:
|
||||||
|
「如该邮箱已绑定账号,您将收到邮件」
|
||||||
|
发送按钮进入 60 秒倒计时" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="250" y="580" width="350" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Decision: Email exists? -->
|
||||||
|
<mxCell id="u8" value="邮箱已绑定 Tenant Admin 账号?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="630" y="475" width="240" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Email found: send email -->
|
||||||
|
<mxCell id="u9" value="后台:异步发送邮件
|
||||||
|
(包含用户名、发送时间)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="640" y="580" width="220" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Email not found: silent -->
|
||||||
|
<mxCell id="u10" value="后台:静默处理
|
||||||
|
(不发送邮件,不报错)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="640" y="660" width="220" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Rate limit check -->
|
||||||
|
<mxCell id="u11" value="同一邮箱 1 小时内已发送 ≥ 3 次?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="630" y="700" width="240" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Rate limit exceeded -->
|
||||||
|
<mxCell id="u12" value="拒绝发送
|
||||||
|
(达到频率上限)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="900" y="715" width="160" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- User receives email -->
|
||||||
|
<mxCell id="u13" value="用户查收邮件,获取用户名" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="640" y="810" width="220" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Return to login -->
|
||||||
|
<mxCell id="u14" value="点击「返回登录」
|
||||||
|
回到登录界面" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="700" width="300" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Edges -->
|
||||||
|
<mxCell id="e1" edge="1" source="u1" target="u2" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e2" edge="1" source="u2" target="u3" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e3" edge="1" source="u3" target="u4" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e4" value="否" edge="1" source="u4" target="u5" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="620" y="385" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e5" value="" edge="1" source="u5" target="u3" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="730" y="285" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e6" value="是" edge="1" source="u4" target="u6" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e7" edge="1" source="u6" target="u7" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e8" edge="1" source="u6" target="u8" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="600" y="500" /><mxPoint x="630" y="510" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e9" value="是" edge="1" source="u8" target="u11" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e10" value="否(普通员工邮箱或不存在)" edge="1" source="u8" target="u10" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint x="760" y="660" as="targetPoint" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e11" value="否(未超限)" edge="1" source="u11" target="u9" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e12" value="是(超过 3 次)" edge="1" source="u11" target="u12" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="900" y="735" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e13" edge="1" source="u9" target="u13" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e14" edge="1" source="u13" target="u14" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="750" y="835" /><mxPoint x="425" y="835" /><mxPoint x="425" y="750" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e15" edge="1" source="u7" target="u14" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
<!-- ============================================================
|
||||||
|
5.4.2 找回密码流程
|
||||||
|
============================================================ -->
|
||||||
|
<diagram id="recover-password" name="5.4.2 找回密码流程">
|
||||||
|
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1"
|
||||||
|
connect="1" arrows="1" fold="1" page="1" pageScale="1"
|
||||||
|
pageWidth="850" pageHeight="1300" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<!-- Title -->
|
||||||
|
<mxCell id="title" value="5.4.2 找回密码流程" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="175" y="20" width="500" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- ===== STEP 1 Header ===== -->
|
||||||
|
<mxCell id="step1hdr" value="步骤一:身份验证" style="text;html=1;strokeColor=none;fillColor=#dae8fc;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=1;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="65" width="730" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Start -->
|
||||||
|
<mxCell id="p1" value="用户点击「忘记密码」" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="115" width="300" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Show form -->
|
||||||
|
<mxCell id="p2" value="展示「找回密码」页面(Stepper)
|
||||||
|
步骤一:用户名 + 绑定邮箱输入框" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="225" y="200" width="400" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- User submits -->
|
||||||
|
<mxCell id="p3" value="用户输入用户名 + 邮箱,点击「下一步」" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="225" y="300" width="400" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Backend verification -->
|
||||||
|
<mxCell id="p4" value="服务端校验用户名与邮箱是否匹配" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="225" y="390" width="400" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Unified response -->
|
||||||
|
<mxCell id="p5" value="统一响应前端:
|
||||||
|
「如信息匹配,重置链接将发送至您的邮箱」" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="225" y="480" width="400" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Decision: Match? -->
|
||||||
|
<mxCell id="p6" value="用户名与邮箱匹配?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="395" width="220" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Rate limit check -->
|
||||||
|
<mxCell id="p7" value="同一账号 1 小时内已发 ≥ 3 次?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="490" width="220" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Matched: generate token -->
|
||||||
|
<mxCell id="p8" value="生成加密 Token
|
||||||
|
(secrets.token_urlsafe(32),有效期 30 分钟)
|
||||||
|
异步发送重置邮件" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="590" width="240" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- No match: silent -->
|
||||||
|
<mxCell id="p9" value="不匹配:静默处理
|
||||||
|
(不发邮件,不报错)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="900" y="405" width="180" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Rate limit exceeded -->
|
||||||
|
<mxCell id="p10" value="已超频率上限
|
||||||
|
静默处理" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="900" y="505" width="160" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- ===== STEP 2 Header ===== -->
|
||||||
|
<mxCell id="step2hdr" value="步骤二:用户点击邮件重置链接" style="text;html=1;strokeColor=none;fillColor=#d5e8d4;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=1;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="690" width="730" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- User clicks link -->
|
||||||
|
<mxCell id="p11" value="用户点击邮件中的重置链接" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="740" width="300" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Validate token -->
|
||||||
|
<mxCell id="p12" value="服务端校验 Token 有效性
|
||||||
|
(is_used=False AND expires_at 未过期)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="250" y="830" width="350" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Token decision -->
|
||||||
|
<mxCell id="p13" value="Token 有效?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="300" y="930" width="250" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Invalid token -->
|
||||||
|
<mxCell id="p14" value="提示「链接已过期或已使用,请重新申请」
|
||||||
|
提供「重新申请」按钮(跳回步骤一)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="620" y="940" width="260" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- ===== STEP 3 Header ===== -->
|
||||||
|
<mxCell id="step3hdr" value="步骤三:输入并提交新密码" style="text;html=1;strokeColor=none;fillColor=#fff2cc;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=1;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="1030" width="730" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Show reset form -->
|
||||||
|
<mxCell id="p15" value="展示「重置密码」表单
|
||||||
|
(新密码 + 确认新密码 + 密码强度指示)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="250" y="1080" width="350" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- User submits new password -->
|
||||||
|
<mxCell id="p16" value="用户输入新密码并提交" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="1180" width="300" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Complexity check -->
|
||||||
|
<mxCell id="p17" value="密码复杂度校验
|
||||||
|
(≥8位,含字母+数字,两次一致)" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="1265" width="300" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Complexity fail -->
|
||||||
|
<mxCell id="p18" value="实时提示不满足的规则
|
||||||
|
(逐条红色 ✗ / 绿色 ✓ 视觉指引)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="640" y="1275" width="240" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- History check -->
|
||||||
|
<mxCell id="p19" value="历史密码校验
|
||||||
|
(不得与最近 3 次历史密码相同)" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="1380" width="300" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- History fail -->
|
||||||
|
<mxCell id="p20" value="提示「不得与最近 3 次密码相同」" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="640" y="1395" width="240" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Success: update password -->
|
||||||
|
<mxCell id="p21" value="✅ 校验通过:
|
||||||
|
① 更新密码(PBKDF2+SHA256 哈希存储)
|
||||||
|
② is_initial_password = False
|
||||||
|
③ 清除该账号所有有效 Session
|
||||||
|
④ 标记 Token 为 is_used = True" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="225" y="1500" width="400" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- End -->
|
||||||
|
<mxCell id="p22" value="跳转登录界面
|
||||||
|
提示「密码已重置,请使用新密码登录」" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="275" y="1630" width="300" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<!-- Edges Step 1 -->
|
||||||
|
<mxCell id="ep1" edge="1" source="p1" target="p2" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep2" edge="1" source="p2" target="p3" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep3" edge="1" source="p3" target="p4" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep4" edge="1" source="p4" target="p5" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep5" edge="1" source="p4" target="p6" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="625" y="415" /><mxPoint x="650" y="430" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep6" value="否" edge="1" source="p6" target="p9" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="900" y="430" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep7" value="是" edge="1" source="p6" target="p7" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep8" value="是(超限)" edge="1" source="p7" target="p10" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="900" y="525" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep9" value="否(未超限)" edge="1" source="p7" target="p8" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<!-- Step 2 edges -->
|
||||||
|
<mxCell id="ep10" edge="1" source="p5" target="p11" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep11" edge="1" source="p11" target="p12" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep12" edge="1" source="p12" target="p13" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep13" value="否(无效/过期)" edge="1" source="p13" target="p14" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="620" y="965" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep14" value="重新申请" edge="1" source="p14" target="p2" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="960" y="970" /><mxPoint x="960" y="230" /><mxPoint x="625" y="230" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep15" value="是(有效)" edge="1" source="p13" target="p15" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<!-- Step 3 edges -->
|
||||||
|
<mxCell id="ep16" edge="1" source="p15" target="p16" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep17" edge="1" source="p16" target="p17" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep18" value="不通过" edge="1" source="p17" target="p18" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="640" y="1305" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep19" value="" edge="1" source="p18" target="p16" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="760" y="1205" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep20" value="通过" edge="1" source="p17" target="p19" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep21" value="不通过" edge="1" source="p19" target="p20" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="640" y="1420" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep22" value="" edge="1" source="p20" target="p16" parent="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="760" y="1205" /></Array></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="ep23" value="通过" edge="1" source="p19" target="p21" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ep24" edge="1" source="p21" target="p22" parent="1"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||