| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- import axios, { AxiosRequestConfig } from 'axios'
- import globalConfig from '@/config/global'
- import { showNotify } from 'vant'
- import 'vant/es/notify/style'
- import type {
- WecomAuthUserInfoResponse,
- WecomBaseResponse,
- WecomExternalContactResponse,
- WecomGroupCheckResult,
- WecomGroupChatResponse,
- WecomInternalUserResponse,
- WecomTokenResponse,
- WecomUserDetailResponse,
- WecomVisitorProfile,
- } from './types'
- const wecomService = axios.create({
- baseURL: '/api',
- timeout: 5000,
- })
- const ACCESS_TOKEN_KEY = 'CommunityFillingAccessToken'
- const ACCESS_TOKEN_EXPIRE_AT_KEY = 'CommunityFillingAccessTokenExpireAt'
- const VISITOR_PROFILE_KEY = 'CommunityFillingWecomVisitorProfile'
- const ONE_DAY_MS = 24 * 60 * 60 * 1000
- const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000
- const INVALID_OAUTH_CODE_ERRCODE = 40029
- type VisitorProfileCacheRecord = {
- expireAt: number
- profile: WecomVisitorProfile
- }
- let refreshTokenPromise: Promise<string> | null = null
- const TOKEN_EXPIRED_ERRCODES = new Set([40014, 42001])
- const wecomActionText: Record<string, string> = {
- gettoken: '企业微信凭证获取失败',
- getuserinfo: '身份获取失败',
- getuserdetail: '成员敏感信息获取失败',
- userGet: '成员信息获取失败',
- externalcontactGet: '外部联系人信息获取失败',
- groupchatGet: '客户群信息获取失败',
- }
- function getCachedAccessToken() {
- const token = localStorage.getItem(ACCESS_TOKEN_KEY) || ''
- const expireAt = Number(localStorage.getItem(ACCESS_TOKEN_EXPIRE_AT_KEY) || 0)
- if (!token || !expireAt) return ''
- if (Date.now() >= expireAt) return ''
- return token
- }
- function cacheAccessToken(token: string, expiresIn = 7200) {
- const safeExpiresIn = Math.max(expiresIn * 1000 - TOKEN_REFRESH_BUFFER_MS, 60 * 1000)
- localStorage.setItem(ACCESS_TOKEN_KEY, token)
- localStorage.setItem(ACCESS_TOKEN_EXPIRE_AT_KEY, String(Date.now() + safeExpiresIn))
- }
- function clearAccessTokenCache() {
- localStorage.removeItem(ACCESS_TOKEN_KEY)
- localStorage.removeItem(ACCESS_TOKEN_EXPIRE_AT_KEY)
- }
- function getExternalContactMobile(resp: WecomExternalContactResponse) {
- return (
- resp.external_contact?.mobile ||
- resp.external_user_profile?.mobile ||
- resp.external_user_profile?.phone ||
- ''
- )
- }
- function showWecomError(action: string, resp: WecomBaseResponse) {
- const defaultText = wecomActionText[action] || '企业微信接口调用失败'
- const detail = resp.errmsg ? `:${resp.errmsg}` : ''
- showNotify({ type: 'warning', message: `${defaultText}${detail}` })
- }
- function shouldRefreshToken(resp?: WecomBaseResponse) {
- return !!resp && TOKEN_EXPIRED_ERRCODES.has(resp.errcode)
- }
- async function requestWecom<T extends WecomBaseResponse>(config: AxiosRequestConfig) {
- const { data } = await wecomService.request<T>(config)
- return data
- }
- async function fetchAccessToken() {
- const resp = await requestWecom<WecomTokenResponse>({
- url: '/cgi-bin/gettoken',
- method: 'GET',
- params: {
- corpid: globalConfig.corpid,
- corpsecret: globalConfig.secret,
- },
- })
- if (resp.errcode !== 0 || !resp.access_token) {
- showWecomError('gettoken', resp)
- throw new Error(resp.errmsg || 'gettoken failed')
- }
- cacheAccessToken(resp.access_token, resp.expires_in)
- return resp.access_token
- }
- export async function getWecomAccessToken(forceRefresh = false) {
- if (!forceRefresh) {
- const cachedToken = getCachedAccessToken()
- if (cachedToken) return cachedToken
- }
- if (!refreshTokenPromise) {
- refreshTokenPromise = fetchAccessToken().finally(() => {
- refreshTokenPromise = null
- })
- }
- return refreshTokenPromise
- }
- async function requestWithAccessToken<T extends WecomBaseResponse>(
- configFactory: (accessToken: string) => AxiosRequestConfig,
- action: string,
- options?: {
- silentErrcodes?: number[]
- },
- ) {
- const accessToken = await getWecomAccessToken()
- let resp = await requestWecom<T>(configFactory(accessToken))
- if (shouldRefreshToken(resp)) {
- clearAccessTokenCache()
- const freshToken = await getWecomAccessToken(true)
- resp = await requestWecom<T>(configFactory(freshToken))
- }
- if (resp.errcode !== 0) {
- if (options?.silentErrcodes?.includes(resp.errcode)) {
- return resp
- }
- showWecomError(action, resp)
- throw new Error(resp.errmsg || `${action} failed`)
- }
- return resp
- }
- export function getWecomUserIdentity(code: string) {
- return requestWithAccessToken<WecomAuthUserInfoResponse>(
- (accessToken) => ({
- url: '/cgi-bin/auth/getuserinfo',
- method: 'GET',
- params: {
- access_token: accessToken,
- code,
- },
- }),
- 'getuserinfo',
- { silentErrcodes: [INVALID_OAUTH_CODE_ERRCODE] },
- )
- }
- export function getWecomInternalUser(userid: string) {
- return requestWithAccessToken<WecomInternalUserResponse>(
- (accessToken) => ({
- url: '/cgi-bin/user/get',
- method: 'GET',
- params: {
- access_token: accessToken,
- userid,
- },
- }),
- 'userGet',
- )
- }
- export function getWecomUserDetail(userTicket: string) {
- return requestWithAccessToken<WecomUserDetailResponse>(
- (accessToken) => ({
- url: '/cgi-bin/auth/getuserdetail',
- method: 'GET',
- params: {
- access_token: accessToken,
- user_ticket: userTicket,
- },
- }),
- 'getuserdetail',
- )
- }
- export function getWecomExternalContact(externalUserId: string) {
- return requestWithAccessToken<WecomExternalContactResponse>(
- (accessToken) => ({
- url: '/cgi-bin/externalcontact/get',
- method: 'GET',
- params: {
- access_token: accessToken,
- external_userid: externalUserId,
- },
- }),
- 'externalcontactGet',
- )
- }
- export function cacheWecomVisitorProfile(profile: WecomVisitorProfile) {
- const record: VisitorProfileCacheRecord = {
- expireAt: Date.now() + ONE_DAY_MS,
- profile,
- }
- localStorage.setItem(VISITOR_PROFILE_KEY, JSON.stringify(record))
- }
- export function getCachedWecomVisitorProfile(): WecomVisitorProfile | null {
- const rawProfile = localStorage.getItem(VISITOR_PROFILE_KEY)
- if (!rawProfile) return null
- let parsed: unknown
- try {
- parsed = JSON.parse(rawProfile)
- } catch {
- localStorage.removeItem(VISITOR_PROFILE_KEY)
- return null
- }
- if (!parsed || typeof parsed !== 'object') {
- localStorage.removeItem(VISITOR_PROFILE_KEY)
- return null
- }
- const rec = parsed as Record<string, unknown>
- if (typeof rec.expireAt === 'number' && rec.profile && typeof rec.profile === 'object') {
- if (Date.now() >= rec.expireAt) {
- localStorage.removeItem(VISITOR_PROFILE_KEY)
- return null
- }
- return rec.profile as WecomVisitorProfile
- }
- if (
- typeof rec.name === 'string' ||
- typeof rec.mobile === 'string' ||
- rec.externalUserId ||
- rec.internalUserid ||
- rec.unionid
- ) {
- const profile = parsed as WecomVisitorProfile
- cacheWecomVisitorProfile(profile)
- return profile
- }
- localStorage.removeItem(VISITOR_PROFILE_KEY)
- return null
- }
- export function getWecomGroupChat(chatId: string, needName = 1) {
- return requestWithAccessToken<WecomGroupChatResponse>(
- (accessToken) => ({
- url: '/cgi-bin/externalcontact/groupchat/get',
- method: 'POST',
- params: {
- access_token: accessToken,
- },
- data: {
- chat_id: chatId,
- need_name: needName,
- },
- }),
- 'groupchatGet',
- )
- }
- function pickMemberUserId(userInfo: WecomAuthUserInfoResponse) {
- const raw = userInfo.userid || userInfo.UserId
- return typeof raw === 'string' && raw ? raw : ''
- }
- export async function checkExternalUserInGroup(chatId: string, unionid: string): Promise<WecomGroupCheckResult> {
- const resp = await requestWithAccessToken<WecomGroupChatResponse>(
- (accessToken) => ({
- url: '/cgi-bin/externalcontact/groupchat/get',
- method: 'POST',
- params: {
- access_token: accessToken,
- },
- data: {
- chat_id: chatId,
- need_name: 1,
- },
- }),
- 'groupchatGet',
- { silentErrcodes: [60020] },
- )
- if (resp.errcode === 60020) {
- return {
- inGroup: false,
- ownerUserId: '',
- groupChat: resp.group_chat,
- }
- }
- const owner = resp.group_chat?.owner
- const ownerUserId = typeof owner === 'string' ? owner : owner?.userid || ''
- const memberList = resp.group_chat?.member_list || []
- const inGroup = memberList.some((item: { unionid?: string }) => item.unionid && item.unionid === unionid)
- return {
- inGroup,
- ownerUserId,
- groupChat: resp.group_chat,
- }
- }
- export async function syncExternalVisitorProfile(code: string) {
- const userInfo = await getWecomUserIdentity(code)
- if (userInfo.errcode === INVALID_OAUTH_CODE_ERRCODE) {
- throw new Error(userInfo.errmsg || 'invalid oauth code')
- }
- const externalUserId = userInfo.external_userid || ''
- if (externalUserId) {
- const externalContact = await getWecomExternalContact(externalUserId)
- const profile: WecomVisitorProfile = {
- name: externalContact.external_contact?.name || '',
- mobile: getExternalContactMobile(externalContact),
- unionid: externalContact.external_contact?.unionid || '',
- externalUserId,
- }
- cacheWecomVisitorProfile(profile)
- return {
- externalUserId,
- profile,
- userInfo,
- externalContact,
- internalUserRaw: undefined,
- }
- }
- const memberUserId = pickMemberUserId(userInfo)
- if (memberUserId) {
- const internal = await getWecomInternalUser(memberUserId)
- let detailRaw: WecomUserDetailResponse | undefined
- let detailMobile = ''
- if (userInfo.user_ticket) {
- try {
- const detail = await getWecomUserDetail(userInfo.user_ticket)
- detailRaw = detail
- detailMobile = (detail.mobile || '').trim()
- } catch {
- detailMobile = ''
- }
- }
- const profile: WecomVisitorProfile = {
- name: internal.name || '',
- mobile: detailMobile || (internal.mobile || internal.telephone || '').trim(),
- unionid: '',
- internalUserid: memberUserId,
- }
- cacheWecomVisitorProfile(profile)
- return {
- externalUserId: '',
- profile,
- userInfo,
- externalContact: null,
- internalUserRaw: detailRaw,
- }
- }
- return {
- externalUserId: '',
- profile: null,
- userInfo,
- externalContact: null,
- internalUserRaw: undefined,
- }
- }
|