group-chat-contact.md 31 KB

企业微信群聊与联系人管理

概述

本文档介绍如何在企业微信聊天窗口的工具栏入口页面中,获取当前会话的群组信息或联系人信息,并同步到 Parse Server 的 GroupChatContact 表中。

引用方式

import { WxworkSDK, WxworkCorp } from 'fmode-ng/core';
import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
import { WxworkAuthGuard } from 'fmode-ng';

核心功能

1. 获取当前聊天上下文

使用 WxworkSDK.getCurrentChat() 方法获取当前企微窗口的会话信息。

2. 同步群组信息

使用 WxworkSDK.syncGroupChat() 方法同步群聊信息到 GroupChat 表。

3. 同步联系人信息

使用 WxworkSDK.syncContact() 方法同步外部联系人信息到 Contact 表。

4. ⭐ 统一获取方法(推荐)

使用 WxworkSDK.getCurrentChatObject() 一次性获取当前会话对象(群聊或联系人)。

WxworkSDK 核心方法

getCurrentChat() - 获取当前会话上下文

/**
 * 获取当前企微窗口的会话信息
 * @returns WxworkCurrentChat 对象
 */
async getCurrentChat(): Promise<WxworkCurrentChat>

interface WxworkCurrentChat {
  type: "chatId" | "userId";  // 会话类型
  id?: string;                 // 群聊ID或用户ID
  group?: any;                 // 群聊详细信息
  contact?: any;               // 联系人详细信息
  follow_user?: any;           // 跟进人信息
}

使用场景

  • 从企微聊天栏工具进入页面时,自动识别当前会话
  • 支持群聊和单聊两种场景

syncGroupChat() - 同步群组信息

/**
 * 同步群聊信息到 GroupChat 表
 * @param groupInfo 企微群聊信息
 * @returns GroupChat Parse Object
 */
async syncGroupChat(groupInfo: any): Promise<FmodeObject>

功能

  • 根据 chat_id 查询或创建 GroupChat 记录
  • 自动生成入群链接 (joinUrl) 和入群二维码 (joinQrcode)
  • 同步群聊基本信息(名称、群主、公告等)
  • 同步成员列表 (member_list) 和成员版本 (member_version)
  • 仅在数据变化时保存,避免不必要的数据库写入

syncContact() - 同步联系人信息

/**
 * 同步外部联系人信息到 Contact 表
 * @param contactInfo 企微外部联系人信息(包含 external_contact 和 follow_user)
 * @returns Contact Parse Object
 */
async syncContact(contactInfo: any): Promise<FmodeObject>

功能

  • 根据 external_useridcompany 查询或创建 Contact 记录
  • 独立字段: name(姓名)、mobile(手机号)、external_userid(外部用户ID)
  • data 字段: 其他所有信息统一存储在 data Object 类型字段中
  • 包含完整的企微联系人信息和 follow_user 跟进人列表
  • 仅在数据变化时保存,避免不必要的数据库写入

参数兼容

// 方式1:传入完整的 contactInfo(推荐)
const contactInfo = await wecorp.externalContact.get(externalUserId);
await wxwork.syncContact(contactInfo);

// 方式2:只传入 external_contact
await wxwork.syncContact(contactInfo.external_contact);

⭐ getCurrentChatObject() - 统一获取方法(推荐)

/**
 * 获取当前聊天对象(群聊或联系人)
 * @returns { GroupChat?, Contact?, currentChat }
 */
async getCurrentChatObject(): Promise<{
  GroupChat?: FmodeObject,
  Contact?: FmodeObject,
  currentChat: WxworkCurrentChat | null
}>

功能

  • 一次调用同时获取会话上下文和对应的 Parse Object
  • 自动识别群聊或联系人场景
  • 自动调用 syncGroupChatsyncContact 同步数据
  • 返回结果包含 GroupChatContact,用户自主判断后续逻辑

返回值

  • GroupChat: 群聊对象(群聊场景下存在)
  • Contact: 联系人对象(单聊场景下存在)
  • currentChat: 原始会话上下文

使用示例

const { GroupChat, Contact, currentChat } = await wxwork.getCurrentChatObject();

if (GroupChat) {
  // 群聊场景
  console.log("群名:", GroupChat.get("name"));
  console.log("成员数:", GroupChat.get("member_list").length);
} else if (Contact) {
  // 联系人场景
  console.log("姓名:", Contact.get("name"));
  console.log("企业:", Contact.get("data").corp_name);
}

数据范式

GroupChat 表(群聊)

字段 类型 说明
chat_id String 企微群聊ID(唯一标识)
name String 群聊名称
owner String 群主 userid
notice String 群公告
member_list Array 群成员列表
member_version String 成员版本号(用于判断成员变化)
joinUrl String 入群链接(scene=1)
joinQrcode String 入群二维码链接(scene=2)
createdAt Date 创建时间
updatedAt Date 更新时间

member_list 结构

[
  {
    "userid": "zhangsan",
    "type": 1,
    "join_time": 1605171726,
    "join_scene": 1,
    "invitor": {
      "userid": "lisi"
    }
  }
]

Contact 表(联系人)

⭐ 数据结构说明

  • 独立字段:仅 name(姓名)、mobile(手机号)、external_userid(外部用户ID)为独立字段
  • data 字段:其他所有企微联系人信息统一存储在 data Object 类型字段中

data 字段包含的信息

{
  "external_userid": "wmxxx",
  "name": "张三",
  "avatar": "http://...",
  "type": 1,
  "gender": 1,
  "unionid": "oxxx",
  "position": "产品经理",
  "corp_name": "ABC公司",
  "corp_full_name": "ABC科技有限公司",
  "external_profile": {
    "external_attr": [
      {
        "type": 0,
        "name": "职位",
        "text": { "value": "产品经理" }
      }
    ]
  },
  "follow_user": [
    {
      "userid": "zhangsan",
      "remark": "备注名",
      "description": "描述",
      "createtime": 1605171726,
      "tags": [
        {
          "group_name": "标签组",
          "tag_name": "标签名",
          "type": 1
        }
      ],
      "remark_corp_name": "备注企业名称",
      "remark_mobiles": ["13800138000"]
    }
  ]
}

访问 data 字段示例

const contact = await wxwork.syncContact(contactInfo);

// 独立字段直接访问
const name = contact.get("name");
const mobile = contact.get("mobile");

// data 字段内容需通过 .get("data") 访问
const data = contact.get("data");
const corpName = data.corp_name;
const avatar = data.avatar;
const followUsers = data.follow_user;

页面开发示例

示例1:群聊消息发送页面

从聊天栏工具入口,自动识别当前群聊并发送消息。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { WxworkSDK, WxworkCorp, WxworkCurrentChat } from 'fmode-ng/core';
import { FmodeParse, FmodeObject } from 'fmode-ng/parse';

const Parse = FmodeParse.with("nova");

export class PageGroupMessageComponent implements OnInit {
  cid: string = '';
  gid: string = '';
  chatId: string = '';

  wework: WxworkSDK;
  wecorp: WxworkCorp;
  currentChat: WxworkCurrentChat | null = null;
  groupChat: FmodeObject | null = null;

  constructor(private route: ActivatedRoute) {}

  async ngOnInit() {
    this.route.paramMap.subscribe(async params => {
      this.cid = params.get('cid') || '';
      this.gid = params.get('gid') || '';

      if (this.cid) {
        this.wework = new WxworkSDK({ cid: this.cid, appId: 'crm' });
        this.wecorp = new WxworkCorp(this.cid);
        await this.loadGroupChat();
      }
    });
  }

  /**
   * 加载群聊信息
   */
  async loadGroupChat() {
    try {
      if (this.gid) {
        // 方式1:通过路由参数 gid 加载
        let query = new Parse.Query("GroupChat");
        this.groupChat = await query.get(this.gid);
        this.chatId = this.groupChat.get("chat_id");

        // 从企微API获取最新群聊信息
        let chatInfo = await this.wecorp.externalContact.groupChat.get(this.chatId);
        this.currentChat = {
          id: chatInfo?.chat_id,
          type: "chatId",
          group: chatInfo?.group_chat || chatInfo
        };
      } else {
        // 方式2:从当前企微窗口获取上下文
        this.currentChat = await this.wework.getCurrentChat();

        if (this.currentChat?.group) {
          this.chatId = this.currentChat.id || '';

          // ⭐ 同步群聊信息到 Parse Server
          this.groupChat = await this.wework.syncGroupChat(this.currentChat.group);
          this.gid = this.groupChat.id;
        }
      }

      console.log("群聊ID:", this.chatId);
      console.log("群聊名称:", this.groupChat?.get("name"));
      console.log("群成员数:", this.groupChat?.get("member_list")?.length);
    } catch (error) {
      console.error('加载群聊失败:', error);
    }
  }

  /**
   * 发送文本消息
   */
  async sendTextMessage() {
    if (!this.chatId) {
      alert('请先加载群聊信息');
      return;
    }

    const content = "这是一条测试消息";
    const result = await this.wecorp.appchat.sendText(this.chatId, content);

    if (result?.errcode === 0) {
      console.log('发送成功');
    } else {
      console.error('发送失败:', result?.errmsg);
    }
  }
}

示例2:联系人详情页面

从联系人侧边栏工具入口,自动识别当前联系人并显示详情。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { WxworkSDK, WxworkCorp, WxworkCurrentChat } from 'fmode-ng/core';
import { FmodeParse, FmodeObject } from 'fmode-ng/parse';

const Parse = FmodeParse.with("nova");

export class PageContactDetailComponent implements OnInit {
  cid: string = '';
  contactId: string = '';
  externalUserId: string = '';

  wework: WxworkSDK;
  wecorp: WxworkCorp;
  currentChat: WxworkCurrentChat | null = null;
  contact: FmodeObject | null = null;

  constructor(private route: ActivatedRoute) {}

  async ngOnInit() {
    this.route.paramMap.subscribe(async params => {
      this.cid = params.get('cid') || '';
      this.contactId = params.get('contactId') || '';

      if (this.cid) {
        this.wework = new WxworkSDK({ cid: this.cid, appId: 'crm' });
        this.wecorp = new WxworkCorp(this.cid);
        await this.loadContact();
      }
    });
  }

  /**
   * 加载联系人信息
   */
  async loadContact() {
    try {
      if (this.contactId) {
        // 方式1:通过路由参数 contactId 加载
        let query = new Parse.Query("Contact");
        this.contact = await query.get(this.contactId);
        this.externalUserId = this.contact.get("external_userid");
      } else {
        // 方式2:从当前企微窗口获取上下文
        this.currentChat = await this.wework.getCurrentChat();

        if (this.currentChat?.contact) {
          this.externalUserId = this.currentChat.id || '';

          // 获取完整的联系人信息
          const contactInfo = await this.wecorp.externalContact.get(this.externalUserId);

          // ⭐ 同步联系人信息到 Parse Server
          this.contact = await this.wework.syncContact(contactInfo.external_contact);
          this.contactId = this.contact.id;
        }
      }

      console.log("联系人ID:", this.externalUserId);
      console.log("联系人姓名:", this.contact?.get("name"));

      // ⭐ 访问 data 字段中的信息
      const data = this.contact?.get("data");
      console.log("所属企业:", data?.corp_name);
      console.log("跟进人:", data?.follow_user);
    } catch (error) {
      console.error('加载联系人失败:', error);
    }
  }

  /**
   * 更新联系人备注
   */
  async updateRemark(remark: string) {
    if (!this.contact) return;

    this.contact.set("remark", remark);
    await this.contact.save();

    console.log('备注更新成功');
  }
}

示例3:⭐ 使用 getCurrentChatObject() 的最简模板(推荐)

适用于快速开发企微工具栏页面,使用统一方法一次性获取会话对象。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { WxworkSDK, WxworkCorp } from 'fmode-ng/core';
import { FmodeObject } from 'fmode-ng/parse';

export class MyToolComponent implements OnInit {
  cid: string = '';
  wework: WxworkSDK;
  wecorp: WxworkCorp;

  groupChat: FmodeObject | null = null;
  contact: FmodeObject | null = null;

  constructor(private route: ActivatedRoute) {}

  async ngOnInit() {
    this.cid = this.route.snapshot.paramMap.get('cid') || '';
    this.wework = new WxworkSDK({ cid: this.cid, appId: 'crm' });
    this.wecorp = new WxworkCorp(this.cid);

    // ⭐ 一次性获取当前会话对象
    const { GroupChat, Contact } = await this.wework.getCurrentChatObject();

    if (GroupChat) {
      // 群聊场景
      this.groupChat = GroupChat;
      console.log("当前群聊:", GroupChat.get("name"));
      console.log("成员数:", GroupChat.get("member_list").length);
    } else if (Contact) {
      // 联系人场景
      this.contact = Contact;
      console.log("当前联系人:", Contact.get("name"));

      // ⭐ 访问 data 字段
      const data = Contact.get("data");
      console.log("企业:", data.corp_name);
      console.log("跟进人:", data.follow_user);
    }

    // 开始业务逻辑...
  }
}

示例4:传统方式(兼容旧代码)

使用 getCurrentChat() 分步处理的方式。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { WxworkSDK, WxworkCorp, WxworkCurrentChat } from 'fmode-ng/core';
import { FmodeObject } from 'fmode-ng/parse';

export class MyToolComponent implements OnInit {
  cid: string = '';
  wework: WxworkSDK;
  wecorp: WxworkCorp;
  currentChat: WxworkCurrentChat | null = null;

  // 群聊或联系人 Parse Object
  chatObject: FmodeObject | null = null;

  constructor(private route: ActivatedRoute) {}

  async ngOnInit() {
    this.cid = this.route.snapshot.paramMap.get('cid') || '';
    this.wework = new WxworkSDK({ cid: this.cid, appId: 'crm' });
    this.wecorp = new WxworkCorp(this.cid);

    // 获取当前会话
    this.currentChat = await this.wework.getCurrentChat();

    if (this.currentChat?.type === "chatId") {
      // 群聊场景
      this.chatObject = await this.wework.syncGroupChat(this.currentChat.group);
      console.log("当前群聊:", this.chatObject.get("name"));
    } else if (this.currentChat?.type === "userId") {
      // 单聊场景
      const contactInfo = await this.wecorp.externalContact.get(this.currentChat.id);
      this.chatObject = await this.wework.syncContact(contactInfo);
      console.log("当前联系人:", this.chatObject.get("name"));
    }

    // 开始业务逻辑...
  }
}

示例5:⭐ 完整的聊天上下文页面(page-chat-context)

这是一个完整的演示页面,展示了如何同时获取和展示员工信息及聊天上下文(群聊或联系人)。

路由配置

  • chat/:cid/context
  • chat/:cid/:appId/context

页面功能

  1. 显示当前登录员工信息(Profile/UserSocial)
  2. 显示当前聊天上下文(群聊或联系人)
  3. 根据场景展示不同的信息卡片

核心代码

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { WxworkSDK, WxworkCorp, WxworkCurrentChat } from 'fmode-ng/core';
import { FmodeParse, FmodeObject } from 'fmode-ng/parse';

const Parse = FmodeParse.with("nova");

@Component({
  selector: 'app-page-chat-context',
  standalone: true,
  templateUrl: './page-chat-context.component.html'
})
export class PageChatContextComponent implements OnInit {
  cid: string = '';
  appId: string = '';

  loading: boolean = true;
  wxwork: WxworkSDK | null = null;
  wecorp: WxworkCorp | null = null;

  // 当前登录的员工信息
  currentUser: FmodeObject | null = null;

  // 当前会话信息
  currentChat: WxworkCurrentChat | null = null;
  chatType: 'group' | 'contact' | 'none' = 'none';

  // 群聊或联系人对象
  groupChat: FmodeObject | null = null;
  contact: FmodeObject | null = null;

  constructor(private route: ActivatedRoute) {}

  async ngOnInit() {
    this.route.paramMap.subscribe(async params => {
      this.cid = params.get('cid') || '';
      this.appId = params.get('appId') || 'crm';

      if (!this.cid) {
        alert('缺少企业ID参数');
        return;
      }

      await this.loadData();
    });
  }

  async loadData() {
    try {
      this.loading = true;

      // 初始化 SDK
      this.wxwork = new WxworkSDK({ cid: this.cid, appId: this.appId });
      this.wecorp = new WxworkCorp(this.cid);

      // 1️⃣ 加载当前登录员工信息(由 WxworkAuthGuard 自动登录)
      this.currentUser = await this.wxwork.getCurrentUser();

      // 2️⃣ 加载当前聊天对象(群聊或联系人)
      const chatObject = await this.wxwork.getCurrentChatObject();
      this.currentChat = chatObject.currentChat;

      if (chatObject.GroupChat) {
        this.chatType = 'group';
        this.groupChat = chatObject.GroupChat;
      } else if (chatObject.Contact) {
        this.chatType = 'contact';
        this.contact = chatObject.Contact;
      }

    } catch (error) {
      console.error('加载数据失败:', error);
      alert('加载失败: ' + error);
    } finally {
      this.loading = false;
    }
  }

  // Helper: 获取当前员工姓名
  getCurrentUserName(): string {
    if (!this.currentUser) return '未知';
    return this.currentUser.get('name') || this.currentUser.get('userid') || '未知';
  }

  // Helper: 获取当前员工类型
  getCurrentUserType(): string {
    if (!this.currentUser) return '';
    return this.currentUser.className === 'Profile' ? '企业员工' : '外部用户';
  }

  // Helper: 获取联系人详细数据
  getContactData(): any {
    return this.contact?.get('data') || {};
  }

  // Helper: 获取联系人企业信息
  getContactCorpName(): string {
    const data = this.getContactData();
    return data.corp_name || data.corp_full_name || '未知';
  }

  // Helper: 获取联系人头像
  getContactAvatar(): string {
    const data = this.getContactData();
    return data.avatar || '';
  }

  // Helper: 获取跟进人列表
  getFollowUsers(): any[] {
    const data = this.getContactData();
    return data.follow_user || [];
  }

  // Helper: 获取群成员列表
  getGroupMembers(): any[] {
    const memberList = this.groupChat?.get('member_list');
    return Array.isArray(memberList) ? memberList : [];
  }
}

模板示例(HTML)

<div class="chat-context-container">
  <!-- 加载状态 -->
  <div *ngIf="loading" class="loading-container">
    <ion-spinner></ion-spinner>
    <p>加载中...</p>
  </div>

  <!-- 内容展示 -->
  <div *ngIf="!loading" class="content">
    <!-- 卡片1: 当前员工信息 -->
    <ion-card>
      <ion-card-header>
        <ion-card-title>
          <ion-icon name="person"></ion-icon>
          当前员工
        </ion-card-title>
      </ion-card-header>
      <ion-card-content>
        <ion-item>
          <ion-label>
            <h3>姓名</h3>
            <p>{{ getCurrentUserName() }}</p>
          </ion-label>
        </ion-item>
        <ion-item>
          <ion-label>
            <h3>类型</h3>
            <p>{{ getCurrentUserType() }}</p>
          </ion-label>
        </ion-item>
      </ion-card-content>
    </ion-card>

    <!-- 卡片2: 群聊信息 -->
    <ion-card *ngIf="chatType === 'group' && groupChat">
      <ion-card-header>
        <ion-card-title>
          <ion-icon name="people"></ion-icon>
          群聊信息
        </ion-card-title>
      </ion-card-header>
      <ion-card-content>
        <ion-item>
          <ion-label>
            <h3>群名</h3>
            <p>{{ groupChat.get('name') }}</p>
          </ion-label>
        </ion-item>
        <ion-item>
          <ion-label>
            <h3>成员数</h3>
            <p>{{ getGroupMembers().length }}</p>
          </ion-label>
        </ion-item>
        <ion-item>
          <ion-label>
            <h3>群主</h3>
            <p>{{ groupChat.get('owner') }}</p>
          </ion-label>
        </ion-item>
      </ion-card-content>
    </ion-card>

    <!-- 卡片3: 联系人信息 -->
    <ion-card *ngIf="chatType === 'contact' && contact">
      <ion-card-header>
        <ion-card-title>
          <ion-icon name="person"></ion-icon>
          联系人信息
        </ion-card-title>
      </ion-card-header>
      <ion-card-content>
        <!-- 头像和基本信息 -->
        <div class="contact-header">
          <ion-avatar *ngIf="getContactAvatar()">
            <img [src]="getContactAvatar()" />
          </ion-avatar>
          <div class="contact-basic">
            <h2>{{ contact.get('name') }}</h2>
            <p>{{ getContactCorpName() }}</p>
          </div>
        </div>

        <!-- 详细信息 -->
        <ion-item>
          <ion-label>
            <h3>手机号</h3>
            <p>{{ contact.get('mobile') || '未绑定' }}</p>
          </ion-label>
        </ion-item>

        <!-- ⭐ 访问 data 字段中的信息 -->
        <h3>企业信息</h3>
        <ion-item>
          <ion-label>
            <h3>企业</h3>
            <p>{{ getContactCorpName() }}</p>
          </ion-label>
        </ion-item>
        <ion-item>
          <ion-label>
            <h3>职位</h3>
            <p>{{ getContactData().position || '未知' }}</p>
          </ion-label>
        </ion-item>

        <!-- 跟进人列表 -->
        <h3>跟进人</h3>
        <div class="follow-users">
          <ion-item *ngFor="let follow of getFollowUsers()">
            <ion-label>
              <h3>{{ follow.userid }}</h3>
              <p *ngIf="follow.remark">备注: {{ follow.remark }}</p>
              <p *ngIf="follow.description">描述: {{ follow.description }}</p>
            </ion-label>
          </ion-item>
        </div>
      </ion-card-content>
    </ion-card>
  </div>
</div>

关键特性

  1. 统一方法调用:使用 getCurrentChatObject() 一次性获取会话对象
  2. 自动场景识别:根据返回的 GroupChatContact 自动判断场景
  3. data 字段访问:通过 getContactData() 统一访问 Contact.data 中的信息
  4. Helper 方法:封装数据访问逻辑,简化模板代码
  5. 响应式设计:适配移动端和桌面端

访问链接示例

入口场景识别

企微工具栏入口有多种场景,可通过 entry 字段判断:

const context = await this.wework.getContext();

switch (context.entry) {
  case "group_chat_tools":
    // 群聊侧边栏工具
    console.log("从群聊工具栏进入");
    break;

  case "single_chat_tools":
    // 单聊侧边栏工具
    console.log("从单聊工具栏进入");
    break;

  case "contact_profile":
    // 联系人详情页
    console.log("从联系人详情进入");
    break;

  case "normal":
    // 普通应用首页
    console.log("从应用首页进入");
    break;
}

常见应用场景

场景1:群发消息工具

async broadcastMessage(message: string) {
  // 获取当前群聊
  const currentChat = await this.wework.getCurrentChat();

  if (currentChat?.type === "chatId") {
    const groupChat = await this.wework.syncGroupChat(currentChat.group);
    const chatId = groupChat.get("chat_id");

    // 发送消息
    await this.wecorp.appchat.sendText(chatId, message);
    console.log("消息已发送到群聊:", groupChat.get("name"));
  }
}

场景2:群成员管理

async listGroupMembers() {
  const currentChat = await this.wework.getCurrentChat();

  if (currentChat?.type === "chatId") {
    const groupChat = await this.wework.syncGroupChat(currentChat.group);
    const memberList = groupChat.get("member_list");

    console.log("群成员总数:", memberList.length);
    memberList.forEach(member => {
      console.log("成员:", member.userid, "加入时间:", member.join_time);
    });
  }
}

场景3:添加客户标签

async addTagToContact(tagIds: string[]) {
  // ⭐ 使用统一方法
  const { Contact } = await this.wework.getCurrentChatObject();

  if (Contact) {
    const externalUserId = Contact.get("external_userid");

    // ⭐ 从 data 字段获取跟进人
    const data = Contact.get("data");
    const userid = data.follow_user[0].userid;

    // 调用企微API添加标签
    await this.wecorp.externalContact.markTag({
      userid: userid,
      external_userid: externalUserId,
      add_tag: tagIds
    });

    console.log("标签已添加到联系人:", Contact.get("name"));
  }
}

场景4:查看客户跟进记录

async showFollowHistory() {
  // ⭐ 使用统一方法
  const { Contact } = await this.wework.getCurrentChatObject();

  if (Contact) {
    // ⭐ 从 data 字段获取跟进人列表
    const data = Contact.get("data");
    const followUsers = data.follow_user || [];

    followUsers.forEach(follow => {
      console.log("跟进人:", follow.userid);
      console.log("备注:", follow.remark);
      console.log("描述:", follow.description);
      console.log("添加时间:", new Date(follow.createtime * 1000));
    });
  }
}

数据同步最佳实践

1. 按需同步

// ✅ 推荐:仅在需要时同步
async loadGroupChat() {
  const currentChat = await this.wework.getCurrentChat();

  if (currentChat?.group) {
    // syncGroupChat 会自动判断是否需要保存
    const groupChat = await this.wework.syncGroupChat(currentChat.group);
    return groupChat;
  }
}

// ❌ 不推荐:频繁无意义的同步
setInterval(async () => {
  const currentChat = await this.wework.getCurrentChat();
  await this.wework.syncGroupChat(currentChat.group); // 浪费资源
}, 1000);

2. 错误处理

async loadContact() {
  try {
    const currentChat = await this.wework.getCurrentChat();

    if (currentChat?.contact) {
      const contactInfo = await this.wecorp.externalContact.get(currentChat.id);
      const contact = await this.wework.syncContact(contactInfo.external_contact);
      return contact;
    } else {
      console.warn("当前不在联系人会话中");
      return null;
    }
  } catch (error) {
    console.error("加载联系人失败:", error);

    // 友好提示
    if (error.code === 84061) {
      alert("无权限访问该联系人信息");
    } else {
      alert("加载失败,请重试");
    }

    return null;
  }
}

3. 缓存策略

export class MyComponent {
  private cachedGroupChat: FmodeObject | null = null;
  private cacheTime: number = 0;
  private CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存

  async getGroupChat(): Promise<FmodeObject> {
    const now = Date.now();

    // 使用缓存
    if (this.cachedGroupChat && (now - this.cacheTime < this.CACHE_DURATION)) {
      return this.cachedGroupChat;
    }

    // 重新加载
    const currentChat = await this.wework.getCurrentChat();
    this.cachedGroupChat = await this.wework.syncGroupChat(currentChat.group);
    this.cacheTime = now;

    return this.cachedGroupChat;
  }
}

权限说明

需要的企微权限

  1. 获取客户详情 - externalcontact:get
  2. 获取客户群详情 - externalcontact_groupchat:get
  3. 发送应用消息 - appchat:send
  4. 企业客户标签管理 - externalcontact_tag:manage

权限检查

async checkPermissions() {
  try {
    // 尝试获取当前会话
    const currentChat = await this.wework.getCurrentChat();

    if (!currentChat) {
      console.error("无法获取会话信息,可能缺少权限");
      return false;
    }

    return true;
  } catch (error) {
    console.error("权限检查失败:", error);
    return false;
  }
}

调试技巧

1. 使用 wxdebug 进行调试

wxdebug 是专门为企微开发设计的调试工具,会在控制台输出并弹窗显示调试信息。

import { wxdebug } from 'fmode-ng/social';

// 调试单个值
const currentChat = await this.wework.getCurrentChat();
wxdebug('当前会话信息', currentChat);

// 调试多个值
wxdebug('步骤1', '初始化完成', { cid: this.cid });

// 调试 Parse Object
const groupChat = await this.wxwork.syncGroupChat(currentChat.group);
wxdebug('群聊同步完成', groupChat.toJSON());

推荐调试流程(参考 page-chat-context):

async loadData() {
  this.wxwork = new WxworkSDK({ cid: this.cid, appId: this.appId });
  wxdebug('1. SDK初始化完成', { cid: this.cid });

  try {
    this.currentChat = await this.wxwork.getCurrentChat();
    wxdebug('2. getCurrentChat返回', this.currentChat);
  } catch (err) {
    wxdebug('2. getCurrentChat失败', err);
  }

  if (this.currentChat?.type === "chatId" && this.currentChat?.group) {
    wxdebug('3. 检测到群聊场景', this.currentChat.group);
    this.groupChat = await this.wxwork.syncGroupChat(this.currentChat.group);
    wxdebug('4. 群聊同步完成', this.groupChat?.toJSON());
  }
}

2. 查看当前会话信息

const currentChat = await this.wework.getCurrentChat();
console.log("会话类型:", currentChat.type);
console.log("会话ID:", currentChat.id);
console.log("详细信息:", currentChat);

3. 查看同步后的数据

const groupChat = await this.wxwork.syncGroupChat(currentChat.group);
console.log("GroupChat Parse Object:", groupChat.toJSON());

4. 模拟不同入口场景

在开发环境可以通过路由参数模拟:

# 群聊场景
/demo/:cid/group/:gid/message

# 联系人场景
/crm/:cid/contact/:contactId/detail

# 聊天上下文演示
/chat/:cid/context

注意事项

  1. getCurrentChat() 依赖企微环境

    • 必须在企微客户端内使用
    • 需要先调用 registerCorpWithSuite() 注册 JSAPI
  2. syncGroupChat() 会生成入群方式

    • 首次同步会调用企微API生成入群链接和二维码
    • 可能需要一定时间,建议显示加载状态
  3. syncContact() 包含跟进人信息

    • follow_user 数组可能为空或包含多个跟进人
    • 跟进人信息包含备注、标签等敏感数据
  4. 数据同步是增量的

    • syncGroupChatsyncContact 只在数据变化时保存
    • 避免频繁调用,建议根据业务需要同步

总结

通过 getCurrentChat()syncGroupChat()syncContact() 三个核心方法,可以轻松实现企微工具栏页面的开发:

一行获取上下文 - await this.wework.getCurrentChat()自动同步数据 - syncGroupChat() / syncContact()统一数据格式 - Parse Server 标准化存储 ✅ 支持离线查询 - 数据持久化到 Parse

开发企微工具栏功能时,只需关注业务逻辑,数据同步由 SDK 自动处理!

字段 类型 说明
external_userid String 外部联系人ID(唯一标识)
company Pointer 所属企业
name String 联系人姓名
mobile String 手机号
data Object 完整的企微联系人信息(包含所有字段)
createdAt Date 创建时间
updatedAt Date 更新时间