# 项目管理模块路由文档

## 概述

本文档描述映三色设计师项目管理系统的企微项目管理模块路由规则，支持从企微端和网页端两种方式进入。

## 核心特性

1. **双端入口支持**
   - 企微端：通过企微 `external_userid` 和 `chat_id` 自动查找关联数据
   - 网页端：通过 `contactId`/`projectId` 直接加载，配合 `profileId` 参数

2. **智能授权**
   - 企微端自动静默授权并同步 Profile
   - 网页端通过 `localStorage` 缓存的 `Parse/ProfileId` 加载
   - 授权过程不阻塞页面加载

3. **灵活加载**
   - 优先使用路由参数
   - 回退到全局 ProfileService
   - 最后尝试企微 SDK 获取

## 路由规则

### 1. 客户画像页

**路由路径**: `/wxwork/:cid/contact/:contactId`

#### 企微端进入

```
/wxwork/cDL6R1hgSi/contact/placeholder?externalUserId=wmKkHgAAF1W7xjKUCcPVdG92Mxxxxxxx
```

**参数说明**:
- `:cid` - 公司帐套ID（必填）
- `:contactId` - ContactInfo 的 objectId，企微端可使用占位符（如 `placeholder`）
- `externalUserId` - 企微外部联系人ID（查询参数，企微端使用）

**加载逻辑**:
1. 通过 `externalUserId` 在 `ContactInfo` 表中查找 `external_userid` 字段匹配的记录
2. 如果找不到，提示"未找到客户信息，请先在企微中添加该客户"
3. 自动触发 WxworkAuth 静默授权，同步当前员工 Profile

**数据要求**:
- `ContactInfo` 必须有 `external_userid` 字段（企微端进入后台才会创建）

---

#### 网页端进入

```
/wxwork/cDL6R1hgSi/contact/abc123xyz?profileId=prof001
```

**参数说明**:
- `:cid` - 公司帐套ID（必填）
- `:contactId` - ContactInfo 的 objectId（必填，真实ID）
- `profileId` - 当前员工 Profile ID（查询参数，可选）

**加载逻辑**:
1. 直接通过 `contactId` 从 `ContactInfo` 表加载数据
2. 优先使用 `profileId` 参数加载当前员工
3. 如果没有 `profileId`，从 `localStorage` 的 `Parse/ProfileId` 加载

**数据要求**:
- `ContactInfo` 必须存在且未软删除（`isDeleted != true`）

---

### 2. 项目详情页

**路由路径**: `/wxwork/:cid/project/:projectId`

#### 企微端进入

```
/wxwork/cDL6R1hgSi/project/placeholder?chatId=wrOtiJDAAAcwMTB7YmDxxxxx
```

**参数说明**:
- `:cid` - 公司帐套ID（必填）
- `:projectId` - Project 的 objectId，企微端可使用占位符（如 `placeholder`）
- `chatId` - 企微群聊 chat_id（查询参数，企微端使用）

**加载逻辑**:
1. 通过 `chatId` 在 `GroupChat` 表中查找 `chat_id` 字段匹配的记录
2. 从 `GroupChat.project` 指针获取关联的 `Project`
3. 如果找不到项目，提示"该群聊尚未关联项目，请先在后台创建项目"
4. 自动触发 WxworkAuth 静默授权，同步当前员工 Profile

**数据要求**:
- `GroupChat` 必须有 `chat_id` 字段（企微群聊同步后创建）
- `GroupChat.project` 指针必须指向有效的 `Project`（后台管理员配置）

---

#### 网页端进入

```
/wxwork/cDL6R1hgSi/project/proj001?profileId=prof001
```

**参数说明**:
- `:cid` - 公司帐套ID（必填）
- `:projectId` - Project 的 objectId（必填，真实ID）
- `profileId` - 当前员工 Profile ID（查询参数，可选）

**加载逻辑**:
1. 直接通过 `projectId` 从 `Project` 表加载数据
2. 优先使用 `profileId` 参数加载当前员工
3. 如果没有 `profileId`，从 `localStorage` 的 `Parse/ProfileId` 加载
4. 自动加载 `Project.customer` 和 `Project.assignee` 关联对象

**数据要求**:
- `Project` 必须存在且未软删除（`isDeleted != true`）

---

## 子路由（项目详情四阶段）

项目详情页包含四个子路由，对应项目管理的四个阶段：

| 路径 | 组件 | 标题 | 说明 |
|------|------|------|------|
| `order` | StageOrderComponent | 订单分配 | 客服下单、分配设计师 |
| `requirements` | StageRequirementsComponent | 确认需求 | 需求沟通、方案确认 |
| `delivery` | StageDeliveryComponent | 交付执行 | 建模、软装、渲染、后期 |
| `aftercare` | StageAftercareComponent | 售后归档 | 尾款结算、客户评价、投诉处理 |

**默认路由**:
- 根据 `Project.currentStage` 自动跳转到对应阶段
- 例如：`currentStage = "建模"` → 跳转到 `delivery`

**阶段映射**:
```typescript
const stageMap = {
  '订单分配': 'order',
  '确认需求': 'requirements',
  '方案确认': 'requirements',
  '建模': 'delivery',
  '软装': 'delivery',
  '渲染': 'delivery',
  '后期': 'delivery',
  '尾款结算': 'aftercare',
  '客户评价': 'aftercare',
  '投诉处理': 'aftercare'
};
```

---

## Profile 获取逻辑

两个组件都使用统一的 `ProfileService` 来获取当前员工信息，优先级如下：

### 优先级 1: 路由参数 `profileId`

```typescript
if (this.profileId) {
  this.currentUser = await this.profileService.getProfileById(this.profileId);
}
```

### 优先级 2: 全局服务（从 localStorage 缓存）

```typescript
if (!this.currentUser) {
  this.currentUser = await this.profileService.getCurrentProfile(this.cid);
}
```

**缓存逻辑**:
1. 检查 `localStorage.getItem("Parse/ProfileId")`
2. 如果存在，从数据库加载 Profile
3. 如果不存在，尝试通过企微授权获取

### 优先级 3: 企微 SDK（企微环境）

```typescript
if (!this.currentUser && this.wxwork) {
  try {
    this.currentUser = await this.wxwork.getCurrentUser();
  } catch (err) {
    console.warn('无法从企微SDK获取用户:', err);
  }
}
```

---

## 企微授权集成

### WxworkAuth 静默授权

两个组件都在 `ngOnInit` 中调用 `initWxworkAuth()` 方法，实现不阻塞页面的静默授权：

```typescript
async initWxworkAuth() {
  if (!this.cid) return;

  try {
    this.wxAuth = new WxworkAuth({ cid: this.cid, appId: 'crm' });

    // 静默授权并同步 Profile，不阻塞页面
    const { profile } = await this.wxAuth.authenticateAndLogin();

    if (profile) {
      this.profileService.setCurrentProfile(profile);
    }
  } catch (error) {
    console.warn('企微授权失败:', error);
    // 授权失败不影响页面加载，继续使用其他方式加载数据
  }
}
```

**特点**:
- 异步执行，不阻塞 `loadData()`
- 授权成功后自动缓存到 `localStorage`
- 授权失败不影响页面正常加载
- 支持自动注册和登录

**授权流程**:
1. 获取企微用户信息（`getUserInfo`）
2. 同步到 `Profile` 或 `UserSocial` 表（`syncUserInfo`）
3. 自动登录/注册（`autoLogin`）
4. 缓存到 `localStorage.setItem("Parse/ProfileId", profile.id)`

---

## 数据表字段要求

### ContactInfo（客户信息表）

| 字段名 | 类型 | 必填 | 说明 | 用途 |
|--------|------|------|------|------|
| objectId | String | 是 | 主键ID | 网页端路由参数 |
| external_userid | String | 否 | 企微外部联系人ID | 企微端查找依据 |
| name | String | 是 | 客户姓名 | 显示使用 |
| company | Pointer | 是 | 所属企业 | 租户隔离 |
| isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |

---

### GroupChat（企微群聊表）

| 字段名 | 类型 | 必填 | 说明 | 用途 |
|--------|------|------|------|------|
| objectId | String | 是 | 主键ID | 唯一标识 |
| chat_id | String | 是 | 企微群聊ID | 企微端查找依据 |
| name | String | 是 | 群聊名称 | 显示使用 |
| company | Pointer | 是 | 所属企业 | 租户隔离 |
| project | Pointer | 否 | 关联项目 | 项目加载 |
| isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |

**重要**: `GroupChat` 必须有 `chat_id` 属性才能支持企微端进入！

---

### Project（项目表）

| 字段名 | 类型 | 必填 | 说明 | 用途 |
|--------|------|------|------|------|
| objectId | String | 是 | 主键ID | 网页端路由参数 |
| title | String | 是 | 项目标题 | 显示使用 |
| customer | Pointer | 是 | 客户 | 关联 ContactInfo |
| assignee | Pointer | 否 | 负责设计师 | 关联 Profile |
| currentStage | String | 是 | 当前阶段 | 默认路由跳转 |
| company | Pointer | 是 | 所属企业 | 租户隔离 |
| isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |

---

### Profile（员工档案表）

| 字段名 | 类型 | 必填 | 说明 | 用途 |
|--------|------|------|------|------|
| objectId | String | 是 | 主键ID | 缓存和查询 |
| name | String | 是 | 员工姓名 | 显示使用 |
| mobile | String | 否 | 手机号 | 联系方式 |
| company | Pointer | 是 | 所属企业 | 租户隔离 |
| userId | String | 否 | 企微UserID | 企微同步 |
| roleName | String | 是 | 员工角色 | 权限控制 |
| isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |

---

## 使用示例

### 1. 从企微群聊进入项目详情

**场景**: 用户在企微群聊中点击应用卡片

```typescript
// 步骤1: 企微 SDK 获取当前群聊
const { GroupChat } = await wxwork.getCurrentChatObject();
const chatId = GroupChat.get('chat_id'); // 例如: "wrOtiJDAAAcwMTB7YmDxxxxx"

// 步骤2: 构造路由
const url = `/wxwork/${cid}/project/placeholder?chatId=${chatId}`;

// 步骤3: 跳转
this.router.navigateByUrl(url);
```

**后台数据准备**:
1. 确保 `GroupChat` 表中有对应 `chat_id` 的记录
2. 确保 `GroupChat.project` 指针指向有效的 `Project`
3. 确保 `Project.customer` 和 `Project.assignee` 已关联

---

### 2. 从后台管理页面进入客户画像

**场景**: 管理员在客户列表中点击查看客户详情

```typescript
// 步骤1: 获取客户 objectId 和当前员工 profileId
const contactId = customer.id; // 例如: "abc123xyz"
const profileId = localStorage.getItem('Parse/ProfileId'); // 例如: "prof001"

// 步骤2: 构造路由
const url = `/wxwork/${cid}/contact/${contactId}?profileId=${profileId}`;

// 步骤3: 跳转
this.router.navigateByUrl(url);
```

---

### 3. 从企微外部联系人进入客户画像

**场景**: 用户在企微中查看外部联系人详情

```typescript
// 步骤1: 企微 SDK 获取外部联系人
const { Contact } = await wxwork.getCurrentChatObject();
const externalUserId = Contact.get('external_userid'); // 例如: "wmKkHgAAF1W7xjKUCcPVdG92Mxxxxxxx"

// 步骤2: 构造路由（使用占位符）
const url = `/wxwork/${cid}/contact/placeholder?externalUserId=${externalUserId}`;

// 步骤3: 跳转
this.router.navigateByUrl(url);
```

**后台数据准备**:
1. 确保已通过企微 API 同步外部联系人到 `ContactInfo` 表
2. 确保 `ContactInfo.external_userid` 字段已填充

---

## 错误处理

### 客户画像页

| 错误场景 | 错误信息 | 解决方案 |
|---------|---------|---------|
| 企微端找不到客户 | "未找到客户信息，请先在企微中添加该客户" | 在企微中添加外部联系人，等待同步 |
| 网页端 contactId 不存在 | "加载失败" | 检查 contactId 是否正确 |
| 没有权限查看敏感信息 | 手机号显示为 `***` | 切换到客服/组长/管理员账号 |

---

### 项目详情页

| 错误场景 | 错误信息 | 解决方案 |
|---------|---------|---------|
| 企微端群聊未关联项目 | "该群聊尚未关联项目，请先在后台创建项目" | 在后台管理页面将群聊关联到项目 |
| 网页端 projectId 不存在 | "加载失败" | 检查 projectId 是否正确 |
| 授权失败 | 控制台警告 | 检查企微配置，或手动传入 profileId |

---

## 权限控制

### 客户画像页

| 权限 | 角色 | 说明 |
|------|------|------|
| 查看基本信息 | 所有角色 | 姓名、来源、画像标签等 |
| 查看敏感信息 | 客服、组长、管理员 | 手机号、微信号 |

### 项目详情页

| 权限 | 角色 | 说明 |
|------|------|------|
| 查看项目信息 | 所有角色 | 项目标题、阶段、进度等 |
| 编辑项目信息 | 客服、组员、组长、管理员 | 更新阶段、上传文件等 |
| 查看客户手机号 | 客服、组长、管理员 | 客户联系方式 |

---

## 技术实现细节

### ProfileService 全局服务

位置: `src/app/services/profile.service.ts`

**核心方法**:
1. `getCurrentProfile(cid?, forceRefresh?)` - 获取当前 Profile
2. `getProfileById(profileId, useCache?)` - 根据 ID 获取 Profile
3. `setCurrentProfile(profile)` - 设置当前 Profile 并缓存
4. `clearCurrentProfile()` - 清除缓存
6. `getCompanyProfiles(companyId, roleName?)` - 获取公司所有员工

---

### WxworkAuth 授权工具

来源: `fmode-ng/core`

**核心方法**:
1. `getUserInfo(code?)` - 获取企微用户信息
2. `syncUserInfo(userInfo?)` - 同步到 Profile 表
3. `autoLogin(userInfo?)` - 自动登录/注册
4. `authenticateAndLogin(code?)` - 一站式授权（推荐使用）

**特点**:
- 静默授权（`snsapi_base`）
- 自动注册用户（用户名=userid，密码=userid后6位）
- 自动同步 Profile 数据
- 缓存到 localStorage

---

## 最佳实践

### 1. 企微端开发

```typescript
// 始终通过企微 SDK 获取 chat_id 或 external_userid
const { GroupChat, Contact } = await wxwork.getCurrentChatObject();

// 使用占位符作为路由参数，避免提前查询
const url = `/wxwork/${cid}/project/placeholder?chatId=${GroupChat.get('chat_id')}`;

// 依赖组件内部的 WxworkAuth 自动授权
this.router.navigateByUrl(url);
```

### 2. 网页端开发

```typescript
// 始终传递 profileId 参数，避免依赖全局缓存
const profileId = localStorage.getItem('Parse/ProfileId');
const url = `/wxwork/${cid}/contact/${contactId}?profileId=${profileId}`;

this.router.navigateByUrl(url);
```

### 3. 数据准备

**企微端**:
1. 同步企微外部联系人到 `ContactInfo` 表（`external_userid` 必填）
2. 同步企微群聊到 `GroupChat` 表（`chat_id` 必填）
3. 在后台管理页面将群聊关联到项目（`GroupChat.project`）

**网页端**:
1. 确保 `ContactInfo` 和 `Project` 数据完整
2. 确保当前员工已登录并缓存 `Parse/ProfileId`
3. 确保员工有权限访问对应数据

---

## 调试技巧

### 1. 检查路由参数

```typescript
console.log('cid:', this.cid);
console.log('contactId:', this.contactId);
console.log('externalUserId:', this.externalUserId);
console.log('profileId:', this.profileId);
```

### 2. 检查 Profile 加载

```typescript
console.log('currentUser:', this.currentUser?.toJSON());
console.log('role:', this.role);
console.log('canEdit:', this.canEdit);
```

### 3. 检查数据加载

```typescript
// 客户画像页
console.log('contactInfo:', this.contactInfo?.toJSON());

// 项目详情页
console.log('project:', this.project?.toJSON());
console.log('groupChat:', this.groupChat?.toJSON());
console.log('customer:', this.customer?.toJSON());
```

### 4. 检查企微授权

```typescript
// 在浏览器控制台查看缓存
console.log('ProfileId:', localStorage.getItem('Parse/ProfileId'));

// 清除缓存重新授权
localStorage.removeItem('Parse/ProfileId');
location.reload();
```

---

## 常见问题

### Q1: 企微端进入提示"未找到客户信息"

**原因**: `ContactInfo` 表中没有对应 `external_userid` 的记录

**解决**:
1. 检查企微外部联系人同步是否成功
2. 检查 `ContactInfo.external_userid` 字段是否填充
3. 检查查询条件是否正确（company、isDeleted）

---

### Q2: 企微端进入提示"该群聊尚未关联项目"

**原因**: `GroupChat.project` 指针为空或指向无效项目

**解决**:
1. 在后台管理页面找到对应群聊
2. 将群聊关联到有效的项目
3. 确保项目未软删除

---

### Q3: 网页端进入后看不到客户手机号

**原因**: 当前员工角色没有权限

**解决**:
1. 检查 `Profile.roleName` 字段
2. 确保角色是"客服"、"组长"或"管理员"
3. 刷新页面重新加载权限

---

### Q4: 授权失败但页面正常加载

**原因**: 授权过程是异步的，不会阻塞页面加载

**说明**:
- 这是正常行为，授权失败会回退到其他加载方式
- 如果需要强制授权，可以使用路由守卫 `WxworkAuthGuard`

---

## 参考文档

- [rules/wxwork/auth.md](../rules/wxwork/auth.md) - 企微授权方法文档
- [rules/schemas.md](../rules/schemas.md) - 数据范式文档
- [docs/schemas.md](./schemas.md) - 数据范式详细文档（如果存在）

---

**文档版本**: v1.0
**最后更新**: 2025-10-17
**维护者**: YSS Development Team
