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 | null = null const TOKEN_EXPIRED_ERRCODES = new Set([40014, 42001]) const wecomActionText: Record = { 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(config: AxiosRequestConfig) { const { data } = await wecomService.request(config) return data } async function fetchAccessToken() { const resp = await requestWecom({ 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( configFactory: (accessToken: string) => AxiosRequestConfig, action: string, options?: { silentErrcodes?: number[] }, ) { const accessToken = await getWecomAccessToken() let resp = await requestWecom(configFactory(accessToken)) if (shouldRefreshToken(resp)) { clearAccessTokenCache() const freshToken = await getWecomAccessToken(true) resp = await requestWecom(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( (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( (accessToken) => ({ url: '/cgi-bin/user/get', method: 'GET', params: { access_token: accessToken, userid, }, }), 'userGet', ) } export function getWecomUserDetail(userTicket: string) { return requestWithAccessToken( (accessToken) => ({ url: '/cgi-bin/auth/getuserdetail', method: 'GET', params: { access_token: accessToken, user_ticket: userTicket, }, }), 'getuserdetail', ) } export function getWecomExternalContact(externalUserId: string) { return requestWithAccessToken( (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 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( (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 { const resp = await requestWithAccessToken( (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, } }