[TOC]
公司使用了Gitlab,Jira等工具來管理,溝通方面主要是釘釘,但郁悶的是各系統(tǒng)相互獨(dú)立,而我已經(jīng)習(xí)慣了前公司那種方式:
有bug的時(shí)候會自動發(fā)送消息到聊天框中,而不是目前這樣,需要開發(fā)人員手動定時(shí)去刷新jira頁面才能知道,效率低下;gitlab也是一樣,有merge請求的時(shí)候,我希望不需要別人提醒我去審核代碼,而是gitlab直接發(fā)送merge消息到我釘釘即可;
可能其他同事習(xí)慣郵件通知吧,公司并無打通各系統(tǒng)與釘釘聯(lián)系的計(jì)劃,所以我只能自己擼一套了,我不是專職后端,輕噴,功能夠我用就好;
已遷移到 掘金, 歡迎關(guān)注;
更新記錄:
1. 2017.04.17 發(fā)現(xiàn)釘釘直接支持幾個(gè)平臺的webhook推送
-
打開釘釘群聊天框右上角的聊天機(jī)器人
聊天機(jī)器人 -
選擇其中需要的平臺
hook列表 -
添加完后再對應(yīng)平臺的設(shè)置中添加webhook地址即可;
但感覺這個(gè)比較粗糙,以jira為例,消息過于精簡,而且通知到群里的話,會讓用戶操心了本不需要操心的內(nèi)容,個(gè)人覺得,這個(gè)比較適合gitlab的merge代碼被通過時(shí)的通知,通知成員用戶更新本地代碼:
jira通知示例
2. 2017.8.30 重構(gòu)項(xiàng)目
使用gradle/kotlin/rxjava/retrofit等改造了之前的項(xiàng)目,支持快速新增gitlab項(xiàng)目部門,手動刷新accessToken及部門信息等功能,部分示例如下,具體請看 項(xiàng)目 :
P.S.重構(gòu)后,war包大小由原來的30+M減小到5M左右 ==!
Github項(xiàng)目地址
相關(guān)文檔
Gitlab webhook document
Jira webhook document
釘釘開放文檔-服務(wù)器端
步驟
- 在
gitlab
上啟用Webhooks
通知(可指定要Webhooks
的操作,這里僅hookmerge
操作,注意:需要項(xiàng)目管理權(quán)限才能設(shè)定,jira
也是類似)
gitlab添加webhook - 在服務(wù)端,根據(jù)post請求的head信息來區(qū)分不同系統(tǒng)發(fā)來的hook消息:
- gitlab的merge請求包含:
X-Gitlab-Event:Merge Request Hook
- jira的hook請求包含:
user-agent:Atlassian HttpClient0.17.3 / JIRA-6.3.15 (6346) / Default
- 在服務(wù)器端打開獲取釘釘?shù)娜藛T信息,并調(diào)用其 企業(yè)會話消息接口 發(fā)送指定信息;
由于該會話接口需要 員工id和企業(yè)應(yīng)用id以及access_token,而 獲取access_token 需要CorpId
和CorpSecret
(二者是企業(yè)的唯一標(biāo)識), 因此可知: - 雖然公司的釘釘后臺上有
CorpId
等信息,但不一定會開放,而等公司組織人員開發(fā)又可能遙遙無期,因此還是自己注冊一下企業(yè),創(chuàng)建部門并添加你想通知的人員作為部門員工即可,這樣也能獲取員工 通訊錄詳情 , 得到用userId,從而發(fā)送釘釘消息; -
需要創(chuàng)建一個(gè)微應(yīng)用,以該應(yīng)用為會話發(fā)起人來發(fā)送消息;
釘釘管理后臺
建立釘釘微應(yīng)用
- 在 釘釘開放平臺 中搜索
微應(yīng)用
就可以找到Step 1 -- 注冊釘釘企業(yè)
的 鏈接; - 根據(jù)上面的
step
引導(dǎo)操作注冊企業(yè)并添加部門和員工,然后進(jìn)入 釘釘管理后臺; - 在
企業(yè)應(yīng)用
標(biāo)簽頁左側(cè)導(dǎo)航條中選擇 微應(yīng)用設(shè)置 即可在右側(cè)看到CorpID
和CorpSecret
; - 在
企業(yè)應(yīng)用
標(biāo)簽下新建應(yīng)用
即可; - 完成后點(diǎn)擊新建的微應(yīng)用圖標(biāo),選擇
設(shè)置
接口查看到微應(yīng)用的AgentID
;
通訊錄規(guī)則
在通訊錄root部門中添加所有人,以便發(fā)送消息到特定用戶時(shí)可以從root部門中通過查詢用戶姓名得到用戶id;
根據(jù)gitlab項(xiàng)目路徑配置各項(xiàng)目部門,比如:
- 假設(shè)gitlab項(xiàng)目地址為: https://gitlab.lynxz.org/demo-android/detail-android
則表示項(xiàng)目名稱(name
) 為:detail-android
,項(xiàng)目所在空間(namespace
)為:demo-android
- 在釘釘后臺通訊錄中需要先創(chuàng)建部門:
demo_android
,然后創(chuàng)建其子部門detail_android
注意: 由于釘釘部門名稱不允許使用-
,因此創(chuàng)建時(shí)改為_
替代 - 目前只支持兩級部門結(jié)構(gòu),若有多個(gè)部門符合上述規(guī)則gitlab merge通過時(shí)會通知所有匹配的部門成員;
備注: 更新釘釘通訊錄后,記得及時(shí)通知server刷新本地?cái)?shù)據(jù),本版支持通過url出發(fā)刷新命令,直接訪問如下網(wǎng)址即可(其中yourServerHost
是war包運(yùn)行后的訪問地址):
{yourServerHost}/action/updateDepartmentInfo
釘釘通訊錄
釘釘發(fā)送消息流程
1. retrofit請求
interface ApiService {
/**
* [獲取釘釘AccessToken](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.dfrJ5p&treeId=172&articleId=104980&docType=1)
* @param id corpid 企業(yè)id
* @param secret corpsecret 企業(yè)應(yīng)用的憑證密鑰
* */
@GET("gettoken")
fun getAccessToken(@Query("corpid") id: String, @Query("corpsecret") secret: String): Observable<AccessTokenBean>
/**
* [獲取部門列表信息](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xIVqtB&treeId=172&articleId=104979&docType=1#s0)
*/
@GET("department/list")
fun getDepartmentList(): Observable<DepartmentListBean>
/**
* [獲取指定部門的成員信息,默認(rèn)獲取全部成員](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xIVqtB&treeId=172&articleId=104979&docType=1#s12)
* */
@GET("user/simplelist")
fun getDepartmentMemberList(@Query("department_id") id: Int = 1): Observable<DepartmentMemberListBean>
/**
* [向指定用戶發(fā)送普通文本消息](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.oavHEu&treeId=172&articleId=104973&docType=1#s2)
*/
@POST("message/send")
fun sendTextMessage(@Body bean: MessageTextBean): Observable<MessageResponseBean>
}
2. 添加必要的request信息
// 給請求添加統(tǒng)一的query參數(shù):access_token
// 這里的ConstantsPara.accessToken是全局變量,存儲獲取到的accessToken
val queryInterceptor = Interceptor { chain ->
val original = chain.request()
val url = original.url().newBuilder()
.addQueryParameter("access_token", ConstantsPara.accessToken)
.build()
val requestBuilder = original.newBuilder().url(url)
chain.proceed(requestBuilder.build())
}
// 給請求添加統(tǒng)一的header參數(shù):Content-Type
val headerInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Content-Type", "application/json")
.build()
chain.proceed(request)
}
val okHttpClient: OkHttpClient = OkHttpClient()
.newBuilder()
.addInterceptor(headerInterceptor)
.addInterceptor(queryInterceptor)
.build()
val ddRetrofit: Retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://oapi.dingtalk.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val apiService: ApiService = ddRetrofit.create(ApiService::class.java)
3. 刷新釘釘?shù)腁ccessToken
apiService.getAccessToken(ConstantsPara.dd_corp_id, ConstantsPara.dd_corp_secret)
.retry(1)
.subscribe(object : Observer<AccessTokenBean> {
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onSubscribe(d: Disposable) {
addDisposable(d)
}
override fun onComplete() {
}
override fun onNext(t: AccessTokenBean) {
println("refreshAccessToken $t")
ConstantsPara.accessToken = t.access_token ?: ""
}
})
4. 獲取部門列表及各部門下的成員信息
部門信息存放在 ConstantsPara.departmentNameMap
中,是一個(gè)hashmap,記錄部門id及名稱,id用于唯一確定部門,以便后續(xù)查找指定部門成員信息;
部門名稱需跟gitlab項(xiàng)目名稱對應(yīng),其中部門id為1的是公司的根部門,主要要將所有人員都添加進(jìn)去,因?yàn)橥ㄖ付ㄈ藛T時(shí),是從根部門中查找用戶姓名,若匹配就發(fā)出消息,而子部門的存在只為適配gitlab項(xiàng)目路徑;
apiService.getDepartmentList()
.flatMap { list ->
ConstantsPara.departmentList = list
list.department.forEach { ConstantsPara.departmentNameMap.put(it.id, it.name) }
Observable.fromIterable(list.department)
}
.map { departmentBean -> departmentBean.id }
.flatMap { departmentId ->
Observable.zip(Observable.create({ it.onNext(departmentId) }),
apiService.getDepartmentMemberList(departmentId),
BiFunction<Int, DepartmentMemberListBean, DepartmentMemberListBean> { t1, t2 ->
t2.departmentId = t1
t2
})
}
.retry(1)
.subscribe(object : Observer<DepartmentMemberListBean> {
override fun onNext(t: DepartmentMemberListBean) {
ConstantsPara.departmentMemberMap.put(t.departmentId, t.userlist)
}
override fun onSubscribe(d: Disposable) {
addDisposable(d)
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onComplete() {
println("getDepartmentInfo onComplete:\n${ConstantsPara.departmentMemberMap.keys.forEach { println("departId: $it") }}")
// sendTextMessage(ConstantsPara.defaultNoticeUserName, "test from server")
}
})
5. 發(fā)送釘釘消息
/**
* 向指定用戶[targetUserName]發(fā)送文本內(nèi)容[message]
* 若目標(biāo)用戶名[targetUserName]為空,則發(fā)送給指定部門[departmentId]所有人,比如gitlab merge請求通過時(shí),通知所有人
* */
fun sendTextMessage(targetUserName: String? = null, message: String = "", departmentId: Int = 1) {
ConstantsPara.departmentMemberMap[departmentId]?.apply {
stream().filter { targetUserName.isNullOrBlank() or it.name.equals(targetUserName, true) }
.forEach {
val textBean = MessageTextBean().apply {
touser = it.userid
agentid = ConstantsPara.dd_agent_id
msgtype = MessageType.TEXT
text = MessageTextBean.TextBean().apply {
content = message
}
}
apiService.sendTextMessage(textBean)
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<MessageResponseBean> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
addDisposable(d)
}
override fun onNext(t: MessageResponseBean) {
println("${msec2date()} sendTextMessage $t")
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
})
}
}
}
其他說明
- 釘釘消息有個(gè) 限制, 因此我在所有消息文本中添加服務(wù)器當(dāng)前時(shí)間,盡量確保每條消息都不同:
forbiddenUserId: 因發(fā)送消息過于頻繁或超量而被流控過濾后實(shí)際未發(fā)送的userid。未被限流的接收者仍會被成功發(fā)送。限流規(guī)則包括:1这敬、給同一用戶發(fā)相同內(nèi)容消息一天僅允許一次;2奥邮、如果是ISV接入方式,給同一用戶發(fā)消息一天不得超過50次纵竖;如果是企業(yè)接入方式漠烧,此上限為500。
- jira的hook信息若是存在
changelog
則表明有用戶修改了issue的狀態(tài)或者內(nèi)容,另外,issuse.comment
一定存在, 數(shù)組comments
存儲了用戶提交的所有備注信息,按時(shí)間先后順序排列; - accessToken的有效期為7200秒,因此項(xiàng)目中需要定時(shí)刷新token;