本文档描述映三色设计师项目管理系统的企微项目管理模块路由规则,支持从企微端和网页端两种方式进入。
双端入口支持
external_userid
和 chat_id
自动查找关联数据contactId
/projectId
直接加载,配合 profileId
参数智能授权
localStorage
缓存的 Parse/ProfileId
加载灵活加载
路由路径: /wxwork/:cid/contact/:contactId
/wxwork/cDL6R1hgSi/contact/placeholder?externalUserId=wmKkHgAAF1W7xjKUCcPVdG92Mxxxxxxx
参数说明:
:cid
- 公司帐套ID(必填):contactId
- ContactInfo 的 objectId,企微端可使用占位符(如 placeholder
)externalUserId
- 企微外部联系人ID(查询参数,企微端使用)加载逻辑:
externalUserId
在 ContactInfo
表中查找 external_userid
字段匹配的记录数据要求:
ContactInfo
必须有 external_userid
字段(企微端进入后台才会创建)/wxwork/cDL6R1hgSi/contact/abc123xyz?profileId=prof001
参数说明:
:cid
- 公司帐套ID(必填):contactId
- ContactInfo 的 objectId(必填,真实ID)profileId
- 当前员工 Profile ID(查询参数,可选)加载逻辑:
contactId
从 ContactInfo
表加载数据profileId
参数加载当前员工profileId
,从 localStorage
的 Parse/ProfileId
加载数据要求:
ContactInfo
必须存在且未软删除(isDeleted != true
)路由路径: /wxwork/:cid/project/:projectId
/wxwork/cDL6R1hgSi/project/placeholder?chatId=wrOtiJDAAAcwMTB7YmDxxxxx
参数说明:
:cid
- 公司帐套ID(必填):projectId
- Project 的 objectId,企微端可使用占位符(如 placeholder
)chatId
- 企微群聊 chat_id(查询参数,企微端使用)加载逻辑:
chatId
在 GroupChat
表中查找 chat_id
字段匹配的记录GroupChat.project
指针获取关联的 Project
数据要求:
GroupChat
必须有 chat_id
字段(企微群聊同步后创建)GroupChat.project
指针必须指向有效的 Project
(后台管理员配置)/wxwork/cDL6R1hgSi/project/proj001?profileId=prof001
参数说明:
:cid
- 公司帐套ID(必填):projectId
- Project 的 objectId(必填,真实ID)profileId
- 当前员工 Profile ID(查询参数,可选)加载逻辑:
projectId
从 Project
表加载数据profileId
参数加载当前员工profileId
,从 localStorage
的 Parse/ProfileId
加载Project.customer
和 Project.assignee
关联对象数据要求:
Project
必须存在且未软删除(isDeleted != true
)项目详情页包含四个子路由,对应项目管理的四个阶段:
路径 | 组件 | 标题 | 说明 |
---|---|---|---|
order |
StageOrderComponent | 订单分配 | 客服下单、分配设计师 |
requirements |
StageRequirementsComponent | 确认需求 | 需求沟通、方案确认 |
delivery |
StageDeliveryComponent | 交付执行 | 建模、软装、渲染、后期 |
aftercare |
StageAftercareComponent | 售后归档 | 尾款结算、客户评价、投诉处理 |
默认路由:
Project.currentStage
自动跳转到对应阶段currentStage = "建模"
→ 跳转到 delivery
阶段映射:
const stageMap = {
'订单分配': 'order',
'确认需求': 'requirements',
'方案确认': 'requirements',
'建模': 'delivery',
'软装': 'delivery',
'渲染': 'delivery',
'后期': 'delivery',
'尾款结算': 'aftercare',
'客户评价': 'aftercare',
'投诉处理': 'aftercare'
};
两个组件都使用统一的 ProfileService
来获取当前员工信息,优先级如下:
profileId
if (this.profileId) {
this.currentUser = await this.profileService.getProfileById(this.profileId);
}
if (!this.currentUser) {
this.currentUser = await this.profileService.getCurrentProfile(this.cid);
}
缓存逻辑:
localStorage.getItem("Parse/ProfileId")
if (!this.currentUser && this.wxwork) {
try {
this.currentUser = await this.wxwork.getCurrentUser();
} catch (err) {
console.warn('无法从企微SDK获取用户:', err);
}
}
两个组件都在 ngOnInit
中调用 initWxworkAuth()
方法,实现不阻塞页面的静默授权:
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
授权流程:
getUserInfo
)Profile
或 UserSocial
表(syncUserInfo
)autoLogin
)localStorage.setItem("Parse/ProfileId", profile.id)
字段名 | 类型 | 必填 | 说明 | 用途 |
---|---|---|---|---|
objectId | String | 是 | 主键ID | 网页端路由参数 |
external_userid | String | 否 | 企微外部联系人ID | 企微端查找依据 |
name | String | 是 | 客户姓名 | 显示使用 |
company | Pointer | 是 | 所属企业 | 租户隔离 |
isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |
字段名 | 类型 | 必填 | 说明 | 用途 |
---|---|---|---|---|
objectId | String | 是 | 主键ID | 唯一标识 |
chat_id | String | 是 | 企微群聊ID | 企微端查找依据 |
name | String | 是 | 群聊名称 | 显示使用 |
company | Pointer | 是 | 所属企业 | 租户隔离 |
project | Pointer | 否 | 关联项目 | 项目加载 |
isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |
重要: GroupChat
必须有 chat_id
属性才能支持企微端进入!
字段名 | 类型 | 必填 | 说明 | 用途 |
---|---|---|---|---|
objectId | String | 是 | 主键ID | 网页端路由参数 |
title | String | 是 | 项目标题 | 显示使用 |
customer | Pointer | 是 | 客户 | 关联 ContactInfo |
assignee | Pointer | 否 | 负责设计师 | 关联 Profile |
currentStage | String | 是 | 当前阶段 | 默认路由跳转 |
company | Pointer | 是 | 所属企业 | 租户隔离 |
isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |
字段名 | 类型 | 必填 | 说明 | 用途 |
---|---|---|---|---|
objectId | String | 是 | 主键ID | 缓存和查询 |
name | String | 是 | 员工姓名 | 显示使用 |
mobile | String | 否 | 手机号 | 联系方式 |
company | Pointer | 是 | 所属企业 | 租户隔离 |
userId | String | 否 | 企微UserID | 企微同步 |
roleName | String | 是 | 员工角色 | 权限控制 |
isDeleted | Boolean | 否 | 软删除标记 | 数据过滤 |
场景: 用户在企微群聊中点击应用卡片
// 步骤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);
后台数据准备:
GroupChat
表中有对应 chat_id
的记录GroupChat.project
指针指向有效的 Project
Project.customer
和 Project.assignee
已关联场景: 管理员在客户列表中点击查看客户详情
// 步骤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);
场景: 用户在企微中查看外部联系人详情
// 步骤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);
后台数据准备:
ContactInfo
表ContactInfo.external_userid
字段已填充错误场景 | 错误信息 | 解决方案 |
---|---|---|
企微端找不到客户 | "未找到客户信息,请先在企微中添加该客户" | 在企微中添加外部联系人,等待同步 |
网页端 contactId 不存在 | "加载失败" | 检查 contactId 是否正确 |
没有权限查看敏感信息 | 手机号显示为 *** |
切换到客服/组长/管理员账号 |
错误场景 | 错误信息 | 解决方案 |
---|---|---|
企微端群聊未关联项目 | "该群聊尚未关联项目,请先在后台创建项目" | 在后台管理页面将群聊关联到项目 |
网页端 projectId 不存在 | "加载失败" | 检查 projectId 是否正确 |
授权失败 | 控制台警告 | 检查企微配置,或手动传入 profileId |
权限 | 角色 | 说明 |
---|---|---|
查看基本信息 | 所有角色 | 姓名、来源、画像标签等 |
查看敏感信息 | 客服、组长、管理员 | 手机号、微信号 |
权限 | 角色 | 说明 |
---|---|---|
查看项目信息 | 所有角色 | 项目标题、阶段、进度等 |
编辑项目信息 | 客服、组员、组长、管理员 | 更新阶段、上传文件等 |
查看客户手机号 | 客服、组长、管理员 | 客户联系方式 |
位置: src/app/services/profile.service.ts
核心方法:
getCurrentProfile(cid?, forceRefresh?)
- 获取当前 ProfilegetProfileById(profileId, useCache?)
- 根据 ID 获取 ProfilesetCurrentProfile(profile)
- 设置当前 Profile 并缓存clearCurrentProfile()
- 清除缓存getCompanyProfiles(companyId, roleName?)
- 获取公司所有员工来源: fmode-ng/core
核心方法:
getUserInfo(code?)
- 获取企微用户信息syncUserInfo(userInfo?)
- 同步到 Profile 表autoLogin(userInfo?)
- 自动登录/注册authenticateAndLogin(code?)
- 一站式授权(推荐使用)特点:
snsapi_base
)// 始终通过企微 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);
// 始终传递 profileId 参数,避免依赖全局缓存
const profileId = localStorage.getItem('Parse/ProfileId');
const url = `/wxwork/${cid}/contact/${contactId}?profileId=${profileId}`;
this.router.navigateByUrl(url);
企微端:
ContactInfo
表(external_userid
必填)GroupChat
表(chat_id
必填)GroupChat.project
)网页端:
ContactInfo
和 Project
数据完整Parse/ProfileId
console.log('cid:', this.cid);
console.log('contactId:', this.contactId);
console.log('externalUserId:', this.externalUserId);
console.log('profileId:', this.profileId);
console.log('currentUser:', this.currentUser?.toJSON());
console.log('role:', this.role);
console.log('canEdit:', this.canEdit);
// 客户画像页
console.log('contactInfo:', this.contactInfo?.toJSON());
// 项目详情页
console.log('project:', this.project?.toJSON());
console.log('groupChat:', this.groupChat?.toJSON());
console.log('customer:', this.customer?.toJSON());
// 在浏览器控制台查看缓存
console.log('ProfileId:', localStorage.getItem('Parse/ProfileId'));
// 清除缓存重新授权
localStorage.removeItem('Parse/ProfileId');
location.reload();
原因: ContactInfo
表中没有对应 external_userid
的记录
解决:
ContactInfo.external_userid
字段是否填充原因: GroupChat.project
指针为空或指向无效项目
解决:
原因: 当前员工角色没有权限
解决:
Profile.roleName
字段原因: 授权过程是异步的,不会阻塞页面加载
说明:
WxworkAuthGuard
文档版本: v1.0 最后更新: 2025-10-17 维护者: YSS Development Team