chain-client/src/service/wechatwork.service.ts

378 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios, { AxiosRequestConfig } from 'axios'
import { singleton } from 'decorators/singleton'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { RedisClient } from 'redis/RedisClient'
import { excelToJson } from 'utils/excel.util'
import { timeBeforeDay } from 'utils/time.util'
// 1-审批中2-已通过3-已驳回4-已撤销6-通过后撤销7-已删除10-已支付
export enum TaskStatus {
PEDING = 1,
PASS = 2,
REJECT = 3,
CANCEL = 4,
PASS_CANCEL = 6,
DELETE = 7,
PAY = 10,
}
const WX_API_HOST = 'https://qyapi.weixin.qq.com'
@singleton
export class WechatWorkService {
private accessToken: string
private tokenExpire: number
private wxToken: string
private wxAesKey: string
private wxCorpId: string
private wxCorpSecret: string
private timePre: number
constructor() {
this.wxToken = process.env.WX_TOKEN
this.wxAesKey = process.env.WX_AES_KEY
this.wxCorpId = process.env.WX_CORP_ID
this.wxCorpSecret = process.env.WX_CORP_SECRET
}
/**
* 获取access_token
* @returns access_token
*/
public async getAccessToken() {
if (!this.accessToken || this.tokenExpire < Date.now()) {
await this.refreshAccessToken()
}
return this.accessToken
}
/**
* 获取微信企业号的access_token
* https://developer.work.weixin.qq.com/resource/devtool
*/
public async refreshAccessToken() {
const url = `${WX_API_HOST}/cgi-bin/gettoken`
let config: AxiosRequestConfig = {
method: 'get',
url,
params: {
corpid: this.wxCorpId,
corpsecret: this.wxCorpSecret,
},
}
let response = await axios.request(config).then(response => {
return response.data
})
if (response.errcode) {
throw new Error(response.errmsg)
}
this.accessToken = response.access_token
this.tokenExpire = Date.now() + response.expires_in * 1000
}
/**
* 获取审批申请详情
* https://developer.work.weixin.qq.com/devtool/interface/alone?id=18615
* @param spNo 审批单号
*/
public async fetchApprovalDetail(spNo: string) {
const url = `${WX_API_HOST}/cgi-bin/oa/getapprovaldetail`
const access_token = await this.getAccessToken()
let config: AxiosRequestConfig = {
method: 'post',
url,
params: {
access_token,
},
data: {
sp_no: spNo,
},
}
let response = await axios.request(config).then(response => {
return response.data
})
return response
}
/**
* 获取用户信息
* @param userid
*/
public async fetchUserInfo(userid: string) {
const url = `${WX_API_HOST}/cgi-bin/user/get`
const access_token = await this.getAccessToken()
let config: AxiosRequestConfig = {
method: 'get',
url,
params: {
access_token,
userid,
},
}
let response = await axios.request(config).then(response => {
return response.data
})
if (response.errcode) {
throw new Error(response.errmsg)
}
return response
}
/**
* 发起一个审核, 用于流程结束时的通知
* @param userid
* @returns
*/
public async beginApproval({
userid,
title,
desc,
info,
}: {
userid: string
title: string
desc: string
info: string
}) {
const url = `${WX_API_HOST}/cgi-bin/oa/applyevent`
const access_token = await this.getAccessToken()
let config: AxiosRequestConfig = {
method: 'post',
url,
params: {
access_token,
},
data: {
creator_userid: userid,
template_id: process.env.WX_NOTIFY_TEMPLATE_ID,
use_template_approver: 0,
approver: [
{
userid: userid,
attr: 1,
},
],
apply_data: {
contents: [
{
control: 'Text',
id: process.env.WX_NOTIFY_TEXT_ID,
value: {
text: title,
},
},
],
},
summary_list: [
{
summary_info: [
{
text: title,
lang: 'zh_CN',
},
],
},
{
summary_info: [
{
text: desc,
lang: 'zh_CN',
},
],
},
{
summary_info: [
{
text: info,
lang: 'zh_CN',
},
],
},
],
},
}
let response = await axios.request(config).then(response => {
return response.data
})
return response
}
/**
* 调用企业微信审核详情接口, 并处理返回数据
* @param spNo 审批单号
*/
public async parseOneTask(spNo: string) {
let detail: any = await this.fetchApprovalDetail(spNo)
if (detail.errcode) {
throw new Error('approval detail error, code: ' + detail.errcode + ' errmsg: ' + detail.errmsg)
}
const { info } = detail
if (info.sp_status !== TaskStatus.PASS) {
throw new Error('approval status error, status: ' + info.sp_status)
}
const { apply_data, applyer } = info
const { contents } = apply_data
let name = ''
let desc = ''
let fileId = ''
let starter = applyer.userid
for (let content of contents) {
let { control, value, title } = content
if (control === 'Text' && title[0].text == '名字') {
name = value.text
} else if (control === 'Text' && title[0].text == '描述') {
desc = value.text
} else if (control === 'File' && value.files.length > 0) {
fileId = value.files[0].file_id
}
}
if (fileId) {
let checked = await this.checkFileMatch(fileId)
if (!checked) {
fileId = ''
}
}
if (!fileId) {
fileId = await this.queryMedidaIdFromSprecord(info.sp_record)
}
if (!fileId) {
fileId = await this.queryMedidaIdFromComment(info.comments)
}
if (!fileId) {
throw new Error('no file')
}
let userInfo = await this.fetchUserInfo(starter)
let starterName = userInfo.name
let { filename } = await this.fetchFile(fileId)
let data = excelToJson(filename)
return { taskId: spNo, name, desc, data, starter, starterName }
}
// 检查审核记录中的文件
private async queryMedidaIdFromSprecord(datas: any) {
let files = []
for (let data of datas) {
if (!data.details || data.details.length === 0) {
continue
}
for (let detail of data.details) {
if (!detail.media_id || detail.media_id.length === 0) {
continue
}
files = files.concat(detail.media_id)
}
}
let result = ''
if (files.length > 0) {
for (let file of files) {
if (await this.checkFileMatch(file)) {
result = file
break
}
}
}
return result
}
// 检查comment中的文件
private async queryMedidaIdFromComment(datas: any) {
let files = []
for (let data of datas) {
if (!data.media_id || data.media_id.length === 0) {
continue
}
files = files.concat(data.media_id)
}
let result = ''
if (files.length > 0) {
for (let file of files) {
if (await this.checkFileMatch(file)) {
result = file
break
}
}
}
return result
}
private async checkFileMatch(mediaId: string) {
let { filename, type } = await this.fetchFile(mediaId, true)
return filename.indexOf('.xlsx') > 0 && type === 'application/octet-stream'
}
/**
* 根据media_id获取文件
* https://developer.work.weixin.qq.com/devtool/interface/alone?id=18615
* @param mediaId
*/
public async fetchFile(mediaId: string, queryOnly = false) {
const source = axios.CancelToken.source()
const url = `${WX_API_HOST}/cgi-bin/media/get`
const access_token = await this.getAccessToken()
let config: AxiosRequestConfig = {
method: 'get',
url,
responseType: 'arraybuffer',
cancelToken: source.token,
params: {
access_token,
media_id: mediaId,
},
}
const res = await axios.request(config)
if (res.status !== 200) {
return { filename: '' }
}
let regex = /.+?filename="(.+?)"/
const match = res.headers['content-disposition'].match(regex)
let remoteName = match ? match[1] : ''
console.log('filename: ' + remoteName + ' type: ' + res.headers['content-type'])
if (queryOnly) {
source.cancel('cancel')
return { filename: remoteName, type: res.headers['content-type'] }
}
let filename = `${mediaId}.xlsx`
const filePath = path.join(os.tmpdir(), filename)
fs.writeFileSync(filePath, res.data)
return { filename: filePath }
}
// 查询审批列表
public async queryTasks() {
const url = `${WX_API_HOST}/cgi-bin/oa/getapprovalinfo`
const access_token = await this.getAccessToken()
let starttime = (timeBeforeDay(7) / 1000) | 0
// if (!this.timePre) {
// let timeStr = await new RedisClient().get('qywx_time_cache')
// if (timeStr) {
// starttime = parseInt(timeStr)
// }
// }
// starttime = starttime || 1683614900
let endtime = (Date.now() / 1000) | 0
let config: AxiosRequestConfig = {
method: 'post',
url,
params: {
access_token,
},
data: {
starttime,
endtime,
cursor: 0,
size: 100,
filters: [
{
key: 'template_id',
value: process.env.WX_TEMPLATE_ID,
},
{
key: 'sp_status',
value: '2',
},
],
},
}
let response = await axios.request(config).then(response => {
return response.data
})
this.timePre = endtime
await new RedisClient().set('qywx_time_cache', endtime + '')
return response
}
}