renzheng il y a 9 ans
Parent
commit
331efc626d

+ 1 - 0
common/src/qfw/util/common.go

@@ -445,6 +445,7 @@ func EndWith(value, str string) bool {
 //出错拦截
 func Catch() {
 	if r := recover(); r != nil {
+		log.Println(r)
 		for skip := 0; ; skip++ {
 			_, file, line, ok := runtime.Caller(skip)
 			if !ok {

+ 10 - 8
weixin/src/config.json

@@ -1,18 +1,18 @@
 {
-	"port":"82",
+	"port":"80",
 	"domain":"127.0.0.1",
 	"imgpath":"E:/go_workspace/qfw/core/src/web/staticres",
-	"mongodbServers": "10.116.86.154:27080",
-	"elasticsearch":"http://10.116.86.154:9800",
+	"mongodbServers": "192.168.3.18:27080",
+	"elasticsearch":"http://192.168.3.18:9800",
     "elasticPoolSize": 30,
 	"mongodbPoolSize":5,
-	"mongodbName":"qfw",	"redisServers":"enterprise=10.116.86.154:1379,service=10.116.86.154:2379,other=10.116.86.154:3379,sso=10.116.86.154:1379,credit=10.116.86.154:4379",
+	"mongodbName":"qfw",	"redisServers":"enterprise=192.168.3.14:1379,service=192.168.3.14:2379,other=192.168.3.14:3379,sso=192.168.3.14:1379,credit=192.168.3.14:4379",
 	"rpcport":"83",
 	"serviceTip":"服务指南",
 	"appcontext":"weixin",
-	"appid":"wx76e1309b01a7b17e",
-	"token":"topnet2015",
-	"appsecret":"dd00e71cb2370432d9de848b674eb8e7",
+	"appid":"wxb8c5625e055bd967",
+	"token":"zzfykc",
+	"appsecret":"288436fdde6349725b4869d54162d3c6",
     "aboutmeurl":"http://www.qimingxing.info/article/aboutme",
     "conactusurl":"http://www.qimingxing.info/article/contactus",
 	"wsqurl": "http://s.p.qq.com/pub/jump?d=AAAXeGLZ",
@@ -54,5 +54,7 @@
 		"out":"您已退出本次工作,辛苦了,再见。",
 		"reply":"已收到您的回复,谢谢。",
 		"msgError":"该功能暂不可用,请稍后再试!"
-	}
+	},
+	"kfprefix":"企明星-",
+	"kfport":"9896"
 }

+ 25 - 3
weixin/src/main.go

@@ -1,8 +1,10 @@
 package main
 
 import (
-	"endless"
+	"encoding/json"
+	//"endless"
 	"log"
+	"net/http"
 	//"net/http"
 	"qfw/util"
 	"qfw/util/elastic"
@@ -32,6 +34,7 @@ func init() {
 	weixin.InitWeixinSdk()
 	//连接消息总线
 	go weixin.InitDgWork()
+	go kf()
 }
 
 func main() {
@@ -40,7 +43,26 @@ func main() {
 	rpc.StartWeixinRpc(weixin.Mux)
 	//启动web服务
 	//http.ListenAndServe(":"+wf.SysConfig.Port, nil) // 启动接收微信数据服务器
-	endless.ListenAndServe(":"+wf.SysConfig.Port, nil, func() {
-		rpc.DestoryRpc()
+	/*
+		endless.ListenAndServe(":"+wf.SysConfig.Port, nil, func() {
+			rpc.DestoryRpc()
+		})
+		**/
+	b := make(chan bool, 1)
+	<-b
+}
+
+func kf() {
+	defer util.Catch()
+	http.HandleFunc("/getkflist", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		b, _ := json.Marshal(weixin.GetKfList(0))
+		w.Write(b)
+	})
+	http.HandleFunc("/getkflist2", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		b, _ := json.Marshal(weixin.GetKfList(1))
+		w.Write(b)
 	})
+	http.ListenAndServe(":"+wf.SysConfig.Kfport, nil)
 }

+ 8 - 1
weixin/src/qfw/weixin/msgtxtchandler.go

@@ -131,7 +131,14 @@ func MsgTxtHandler(w ResponseWriter, r *Request) {
 			dao.SaveWeixinOfflineMessage(r.FromUserName, r.Content, now.Unix())
 			w.ReplyText(wf.SysConfig.WeixinAutoRpl)
 		} else {
-			w.Reply2CustomerService()
+			//w.Reply2CustomerService()
+			//转接到平台客服
+			s := GetMinKf()
+			if s == "" {
+				w.Reply2CustomerService()
+			} else {
+				w.TransferCustomerService2(r.FromUserName, GetMinKf())
+			}
 		}
 	}
 }

+ 263 - 0
weixin/src/qfw/weixin/weixincustomer.go

@@ -0,0 +1,263 @@
+package weixin
+
+import (
+	"encoding/json"
+	"log"
+	"qfw/util"
+	"qfw/util/mongodb"
+	"qfw/weixinconfig"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+)
+
+//定时获取客服状态
+//客服分类,平台客服 企明星-小微,业务客服 工商注册-小张
+//处理客服类型
+/**
+{
+   "kf_online_list": [
+       {
+           "kf_account": "test1@test",
+           "status": 1,
+           "kf_id": "1001",
+           "accepted_case": 1
+       },
+       {
+           "kf_account": "test2@test",
+           "status": 1,
+           "kf_id": "1002",
+           "accepted_case": 2
+       }
+   ]
+}
+**/
+
+//通过url获取在线客服信息
+func GetKfList(t int) (res []map[string]interface{}) {
+	defer util.Catch()
+	ONLINE_KF_LOCK.Lock()
+	defer ONLINE_KF_LOCK.Unlock()
+	res = []map[string]interface{}{}
+	defer util.Catch()
+	l := Mux.GetOnlineKfList()
+	log.Println("===", l)
+	kf_online_list := l["kf_online_list"].([]interface{})
+	if len(kf_online_list) > 0 {
+		GetKfInfoByMap(t)
+		QMX_ONLINE_KF = arr{}
+		for _, onlist := range kf_online_list {
+			tmp := onlist.(map[string]interface{})
+			kf_account := tmp["kf_account"].(string)
+			onekf := KF_INFO[kf_account]
+			if onekf == nil {
+				log.Println("数据库客服列表不完整...")
+			} else {
+				for k, v := range onekf {
+					tmp[k] = v
+				}
+			}
+			kf_nick := tmp["kf_nick"].(string)
+			if strings.HasPrefix(kf_nick, weixinconfig.SysConfig.Kfprefix) {
+				k := map[string]int{}
+				k[tmp["kf_account"].(string)] = util.IntAll(tmp["accepted_case"])
+				QMX_ONLINE_KF = append(QMX_ONLINE_KF)
+			}
+			res = append(res, tmp)
+		}
+		//排序
+		sort.Sort(&QMX_ONLINE_KF)
+	}
+	log.Println(res)
+	return
+}
+
+//定时更新在线客服
+func GetOnlineKfJob() {
+	hour := time.Hour
+	if hour > 7 && hour < 18 {
+		GetKfList(0)
+	}
+	time.AfterFunc(1*time.Minute, GetOnlineKfJob)
+}
+
+//获取最少的客服
+func GetMinKf() string {
+	ONLINE_KF_LOCK.Lock()
+	defer ONLINE_KF_LOCK.Unlock()
+	res := ""
+	if QMX_ONLINE_KF.Len() > 0 {
+		for res, _ = range QMX_ONLINE_KF[0] {
+			break
+		}
+	}
+	return res
+}
+
+type arr []map[string]int
+
+func (a *arr) Len() int {
+	return len(*a)
+}
+func (a *arr) Less(i, j int) bool {
+	return (*a)[i]["accepted_case"] > (*a)[j]["accepted_case"]
+}
+func (a *arr) Swap(i, j int) {
+	tmp := (*a)[i]
+	(*a)[i] = (*a)[j]
+	(*a)[j] = tmp
+}
+
+/**
+{
+    "kf_list" : [
+       {
+          "kf_account" : "test1@test",
+          "kf_headimgurl" : "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjfUS8Ym0GSaLic0FD3vN0V8PILcibEGb2fPfEOmw/0",
+          "kf_id" : "1001",
+          "kf_nick" : "ntest1",
+          "kf_wx" : "kfwx1"
+       }]
+}
+**/
+const (
+	KF_COLLECTION    = "wxkf_info"
+	KFMSG_COLLECTION = "wxkf_msg"
+)
+
+var Last_kf_time = int64(0) //获取客服的最后时间
+var KF_INFO = map[string]map[string]interface{}{}
+var KF_LOCK = sync.Mutex{}                          //客服信息锁
+var ONLINE_KF_LOCK = sync.Mutex{}                   //在线客服锁
+var ONLINE_KF = map[string]map[string]interface{}{} //所有在线客服
+var QMX_ONLINE_KF = arr{}                           //平台在线客服
+var MSG_LOCL = sync.Mutex{}                         //客服消息锁
+var Last_msg_time = int64(0)                        //获取聊天记录的最后时间
+
+//获取客服信息
+func GetKfInfoByApi() {
+	log.Println("getkf---")
+	defer util.Catch()
+	KF_LOCK.Lock()
+	defer KF_LOCK.Unlock()
+	if time.Now().Unix()-Last_kf_time > 60 {
+		Last_kf_time = time.Now().Unix()
+		l := Mux.GetKfList()
+		log.Println(l)
+		if l != nil && len(l) > 0 {
+			kf_list := l["kf_list"].([]interface{})
+			if len(kf_list) > 0 {
+				KF_INFO = map[string]map[string]interface{}{}
+				for _, kf := range kf_list {
+					onekf := kf.(map[string]interface{})
+					kf_account := onekf["kf_account"].(string)
+					KF_INFO[kf_account] = onekf
+				}
+				if mongodb.Del(KF_COLLECTION, nil) {
+					if !mongodb.SaveBulk(KF_COLLECTION, util.ObjArrToMapArr(kf_list)...) {
+						time.Sleep(10 * time.Second)
+						mongodb.SaveBulk(KF_COLLECTION, util.ObjArrToMapArr(kf_list)...)
+					}
+				}
+			}
+		}
+	}
+}
+
+//从内存中或Url获取客服信息/0从内存 /1从url
+func GetKfInfoByMap(t int) map[string]map[string]interface{} {
+	if t == 0 {
+		if len(KF_INFO) == 0 {
+			KF_LOCK.Lock()
+			defer KF_LOCK.Unlock()
+			res := mongodb.Find(KF_COLLECTION, nil, nil, nil, false, -1, -1)
+			if res != nil && *res != nil && len(*res) > 0 {
+				for _, kf := range *res {
+					kf_account := kf["kf_account"].(string)
+					KF_INFO[kf_account] = kf
+				}
+			} else {
+				GetKfInfoByApi()
+			}
+		}
+	} else if t == 1 {
+		GetKfInfoByApi()
+	}
+	return KF_INFO
+}
+
+type Msg struct {
+	MsgId     int64 `json:"msgid"`
+	StartTime int64 `json:"starttime"`
+	EndTime   int64 `json:"endtime"`
+	Number    int   `json:"number"`
+}
+
+//获取客服聊天记录
+//查询时间不能超过24小时
+func GetKfMsg() {
+	defer util.Catch()
+	MSG_LOCL.Lock()
+	defer MSG_LOCL.Unlock()
+	now := time.Now().Unix() - 3*60
+	if Last_msg_time == 0 {
+		res := mongodb.Find(KFMSG_COLLECTION, nil, `{"l_gettime":-1}`, `{"l_gettime":1}`, false, 0, 1)
+		if res != nil && *res != nil && len(*res) == 1 {
+			Last_msg_time = util.Int64All((*res)[0]["l_gettime"])
+		}
+		if Last_msg_time == 0 || now-Last_msg_time > 86400 {
+			Last_msg_time = now - 86400
+		}
+	}
+	msg := Msg{}
+	msg.MsgId = 1
+	msg.Number = 2000
+	msg.EndTime = now
+	msg.StartTime = Last_msg_time
+	for i := 0; i < 15; i++ {
+		bs, err := Mux.PostCustomMsg("https://api.weixin.qq.com/customservice/msgrecord/getmsglist?access_token=", msg)
+		if err == nil {
+			var res map[string]interface{}
+			_err := json.Unmarshal(bs, &res)
+			if _err == nil && res != nil {
+				recordlist := res["recordlist"].([]interface{})
+				recMap := []map[string]interface{}{}
+				for _, record := range recordlist {
+					tmp := record.(map[string]interface{})
+					tmp["l_gettime"] = now
+				}
+				//保存客服消息
+				if len(recMap) > 0 {
+					if !mongodb.SaveBulk(KFMSG_COLLECTION, recMap...) {
+						break
+					}
+				}
+				num := util.IntAll(res["number"])
+				mid := util.Int64All(res["msgid"])
+				log.Println(num, mid, i, len(recMap))
+				if num < msg.Number {
+					break
+				} else {
+					msg.MsgId = mid
+				}
+			} else {
+				log.Println(_err)
+				break
+			}
+		} else {
+			log.Println(err)
+			break
+		}
+		time.Sleep(1 * time.Second)
+	}
+}
+
+//定时抓取客服记录
+func GetKfMsgJob() {
+	hour := time.Hour
+	if hour > 7 && hour < 18 {
+		GetKfMsg()
+	}
+	time.AfterFunc(2*time.Minute, GetKfMsgJob)
+}

+ 178 - 106
weixin/src/qfw/weixin/weixinsdk.go

@@ -18,7 +18,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"sort"
-	"strings"
+	"sync/atomic"
 	"time"
 )
 
@@ -27,7 +27,7 @@ const (
 	msgEvent          = "event"
 	EventSubscribe    = "subscribe"
 	EventUnsubscribe  = "unsubscribe"
-	EventScan         = "scan"
+	EventScan         = "SCAN"
 	EventView         = "VIEW"
 	EventClick        = "CLICK"
 	EventLocation     = "LOCATION"
@@ -78,6 +78,7 @@ const (
 	weixinHost               = "https://api.weixin.qq.com/cgi-bin"
 	weixinQRScene            = "https://api.weixin.qq.com/cgi-bin/qrcode"
 	weixinShowQRScene        = "https://mp.weixin.qq.com/cgi-bin/showqrcode"
+	weixinMaterialURL        = "https://api.weixin.qq.com/cgi-bin/material"
 	weixinShortURL           = "https://api.weixin.qq.com/cgi-bin/shorturl"
 	weixinUserInfo           = "https://api.weixin.qq.com/cgi-bin/user/info"
 	weixinFileURL            = "http://file.api.weixin.qq.com/cgi-bin/media"
@@ -88,17 +89,19 @@ const (
 	// Max retry count
 	retryMaxN = 3
 	// Reply format
-	replyText               = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
-	replyImage              = "<xml>%s<MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>"
-	replyVoice              = "<xml>%s<MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>"
-	replyVideo              = "<xml>%s<MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[%s]]></MediaId><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description></Video></xml>"
-	replyMusic              = "<xml>%s<MsgType><![CDATA[music]]></MsgType><Music><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><MusicUrl><![CDATA[%s]]></MusicUrl><HQMusicUrl><![CDATA[%s]]></HQMusicUrl><ThumbMediaId><![CDATA[%s]]></ThumbMediaId></Music></xml>"
-	replyNews               = "<xml>%s<MsgType><![CDATA[news]]></MsgType><ArticleCount>%d</ArticleCount><Articles>%s</Articles></xml>"
-	replyHeader             = "<ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime>"
-	replyArticle            = "<item><Title><![CDATA[%s]]></Title> <Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>"
-	transferCustomerService = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
-	reply2CustomerService   = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
-
+	replyText                = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
+	replyImage               = "<xml>%s<MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>"
+	replyVoice               = "<xml>%s<MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>"
+	replyVideo               = "<xml>%s<MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[%s]]></MediaId><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description></Video></xml>"
+	replyMusic               = "<xml>%s<MsgType><![CDATA[music]]></MsgType><Music><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><MusicUrl><![CDATA[%s]]></MusicUrl><HQMusicUrl><![CDATA[%s]]></HQMusicUrl><ThumbMediaId><![CDATA[%s]]></ThumbMediaId></Music></xml>"
+	replyNews                = "<xml>%s<MsgType><![CDATA[news]]></MsgType><ArticleCount>%d</ArticleCount><Articles>%s</Articles></xml>"
+	replyHeader              = "<ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime>"
+	replyArticle             = "<item><Title><![CDATA[%s]]></Title> <Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>"
+	transferCustomerService  = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
+	transferCustomerService2 = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType><TransInfo><KfAccount><![CDATA[%s]]></KfAccount></TransInfo></xml>"
+	reply2CustomerService    = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
+	// Material request
+	requestMaterial = `{"type":"%s","offset":%d,"count":%d}`
 	// QR scene request
 	requestQRScene      = `{"expire_seconds":%d,"action_name":"QR_SCENE","action_info":{"scene":{"scene_id":%d}}}`
 	requestQRLimitScene = `{"action_name":"QR_LIMIT_SCENE","action_info":{"scene":{"scene_id":%d}}}`
@@ -200,6 +203,38 @@ type UserInfo struct {
 	GroupId       int    `json:"groupid,omitempty"`
 }
 
+type Material struct {
+	MediaId    string `json:"media_id,omitempty"`
+	Name       string `json:"name,omitempty"`
+	UpdateTime int64  `json:"update_time,omitempty"`
+	CreateTime int64  `json:"create_time,omitempty"`
+	Url        string `json:"url,omitempty"`
+	Content    struct {
+		NewsItem []struct {
+			Title            string `json:"title,omitempty"`
+			ThumbMediaId     string `json:"thumb_media_id,omitempty"`
+			ShowCoverPic     int    `json:"show_cover_pic,omitempty"`
+			Author           string `json:"author,omitempty"`
+			Digest           string `json:"digest,omitempty"`
+			Content          string `json:"content,omitempty"`
+			Url              string `json:"url,omitempty"`
+			ContentSourceUrl string `json:"content_source_url,omitempty"`
+		} `json:"news_item,omitempty"`
+	} `json:"content,omitempty"`
+}
+
+type Materials struct {
+	TotalCount int        `json:"total_count,omitempty"`
+	ItemCount  int        `json:"item_count,omitempty"`
+	Items      []Material `json:"item,omitempty"`
+}
+
+type TmplData map[string]TmplItem
+type TmplItem struct {
+	Value string `json:"value,omitempty"`
+	Color string `json:"color,omitempty"`
+}
+
 // Use to output reply
 type ResponseWriter interface {
 	// Get weixin
@@ -208,12 +243,14 @@ type ResponseWriter interface {
 	// Reply message
 	ReplyOK()
 	ReplyText(text string)
-	Reply2CustomerService()
 	ReplyImage(mediaId string)
 	ReplyVoice(mediaId string)
 	ReplyVideo(mediaId string, title string, description string)
 	ReplyMusic(music *Music)
 	ReplyNews(articles []Article)
+
+	Reply2CustomerService()
+
 	TransferCustomerService(serviceId string)
 	// Post message
 	PostText(text string) error
@@ -222,12 +259,14 @@ type ResponseWriter interface {
 	PostVideo(mediaId string, title string, description string) error
 	PostMusic(music *Music) error
 	PostNews(articles []Article) error
-	PostTemplateMessage(templateid string, url string, data interface{}) (string, error)
+	PostTemplateMessage(templateid string, url string, data TmplData) (int32, error)
 	// Media operator
 	UploadMediaFromFile(mediaType string, filepath string) (string, error)
 	DownloadMediaToFile(mediaId string, filepath string) error
 	UploadMedia(mediaType string, filename string, reader io.Reader) (string, error)
 	DownloadMedia(mediaId string, writer io.Writer) error
+	//
+	TransferCustomerService2(serviceId, kfaccount string)
 	GetUserBaseInfo(openid string) (map[string]interface{}, error)
 }
 
@@ -262,17 +301,16 @@ type jsApiTicket struct {
 }
 
 type Weixin struct {
-	token      string
-	routes     []*route
-	tokenChan  chan accessToken
-	ticketChan chan jsApiTicket
-	userData   interface{}
-	appId      string
-	appSecret  string
+	token        string
+	routes       []*route
+	tokenChan    chan accessToken
+	ticketChan   chan jsApiTicket
+	userData     interface{}
+	appId        string
+	appSecret    string
+	refreshToken int32
 }
 
-//
-
 // Convert qr scene to url
 func (qr *QRScene) ToURL() string {
 	return (weixinShowQRScene + "?ticket=" + qr.Ticket)
@@ -284,9 +322,10 @@ func New(token string, appid string, secret string) *Weixin {
 	wx.token = token
 	wx.appId = appid
 	wx.appSecret = secret
+	wx.refreshToken = 0
 	if len(appid) > 0 && len(secret) > 0 {
 		wx.tokenChan = make(chan accessToken)
-		go createAccessToken(wx.tokenChan, appid, secret)
+		go wx.createAccessToken(wx.tokenChan, appid, secret)
 		wx.ticketChan = make(chan jsApiTicket)
 		go createJsApiTicket(wx.tokenChan, wx.ticketChan)
 	}
@@ -307,6 +346,11 @@ func (wx *Weixin) GetAppSecret() string {
 	return wx.appSecret
 }
 
+func (wx *Weixin) RefreshAccessToken() {
+	atomic.StoreInt32(&wx.refreshToken, 1)
+	<-wx.tokenChan
+}
+
 // Register request callback.
 func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
 	regex, err := regexp.Compile(pattern)
@@ -333,28 +377,6 @@ func (wx *Weixin) PostText(touser string, text string) error {
 	return postMessage(wx.tokenChan, &msg)
 }
 
-//Post custom message(消息结构体完全由开发人员自定义)
-func (wx *Weixin) PostTextCustom(url string, obj interface{}) error {
-	data, err := json.Marshal(obj)
-	if err != nil {
-		return err
-	}
-	//log.Println("custom msg:", string(data))
-	_, err = postRequest(url, wx.tokenChan, data)
-	return err
-}
-
-//Post custom message(消息结构体完全由开发人员自定义)
-func (wx *Weixin) PostCustomMsg(url string, obj interface{}) (bs []byte, err error) {
-	var data []byte
-	data, err = json.Marshal(obj)
-	if err != nil {
-		return
-	}
-	bs, err = postRequest(url, wx.tokenChan, data)
-	return
-}
-
 // Post image message
 func (wx *Weixin) PostImage(touser string, mediaId string) error {
 	var msg struct {
@@ -462,6 +484,20 @@ func (wx *Weixin) DownloadMedia(mediaId string, writer io.Writer) error {
 	return downloadMedia(wx.tokenChan, mediaId, writer)
 }
 
+// Batch Get Material
+func (wx *Weixin) BatchGetMaterial(materialType string, offset int, count int) (*Materials, error) {
+	reply, err := postRequest(weixinMaterialURL+"/batchget_material?access_token=", wx.tokenChan,
+		[]byte(fmt.Sprintf(requestMaterial, materialType, offset, count)))
+	if err != nil {
+		return nil, err
+	}
+	var materials Materials
+	if err := json.Unmarshal(reply, &materials); err != nil {
+		return nil, err
+	}
+	return &materials, nil
+}
+
 // Get ip list
 func (wx *Weixin) GetIpList() ([]string, error) {
 	reply, err := sendGetRequest(weixinHost+"/getcallbackip?access_token=", wx.tokenChan)
@@ -530,16 +566,12 @@ func (wx *Weixin) ShortURL(url string) (string, error) {
 
 // Custom menu
 func (wx *Weixin) CreateMenu(menu *Menu) error {
-	data, err := json.Marshal(menu)
+	data, err := marshal(menu)
 	if err != nil {
 		return err
-	} else {
-		tmp := string(data)
-		tmp = strings.Replace(tmp, "\\u0026", "&", -1)
-		log.Println("MenuJson", string(tmp))
-		_, err := postRequest(weixinHost+"/menu/create?access_token=", wx.tokenChan, []byte(tmp))
-		return err
 	}
+	_, err = postRequest(weixinHost+"/menu/create?access_token=", wx.tokenChan, data)
+	return err
 }
 
 func (wx *Weixin) GetMenu() (*Menu, error) {
@@ -599,12 +631,12 @@ func (wx *Weixin) AddTemplate(shortid string) (string, error) {
 	return templateId.Id, nil
 }
 
-func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data interface{}) (string, error) {
+func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data TmplData) (int32, error) {
 	var msg struct {
-		ToUser     string      `json:"touser"`
-		TemplateId string      `json:"template_id"`
-		Url        string      `json:"url,omitempty"`
-		Data       interface{} `json:"data,omitempty"`
+		ToUser     string   `json:"touser"`
+		TemplateId string   `json:"template_id"`
+		Url        string   `json:"url,omitempty"`
+		Data       TmplData `json:"data,omitempty"`
 	}
 	msg.ToUser = touser
 	msg.TemplateId = templateid
@@ -612,17 +644,17 @@ func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url stri
 	msg.Data = data
 	msgStr, err := marshal(msg)
 	if err != nil {
-		return "", err
+		return 0, err
 	}
 	reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
 	if err != nil {
-		return "", err
+		return 0, err
 	}
 	var resp struct {
-		MsgId string `json:"msgid,omitempty"`
+		MsgId int32 `json:"msgid,omitempty"`
 	}
 	if err := json.Unmarshal(reply, &resp); err != nil {
-		return "", err
+		return 0, err
 	}
 	return resp.MsgId, nil
 }
@@ -708,7 +740,6 @@ func (wx *Weixin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		log.Println("Weixin receive message failed:", err)
 		http.Error(w, "", http.StatusBadRequest)
 	} else {
-		//log.Println("recive data:", string(data))
 		var msg Request
 		if err := xml.Unmarshal(data, &msg); err != nil {
 			log.Println("Weixin parse message failed:", err)
@@ -764,7 +795,6 @@ func checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
 	}
 	h := sha1.New()
 	h.Write([]byte(str))
-	//fmt.Printf("checksignature:%s %s \n %s %s \n", str, timestamp, signature, nonce)
 	return fmt.Sprintf("%x", h.Sum(nil)) == signature
 }
 
@@ -812,11 +842,12 @@ func getJsApiTicket(c chan accessToken) (*jsApiTicket, error) {
 
 }
 
-func createAccessToken(c chan accessToken, appid string, secret string) {
+func (wx *Weixin) createAccessToken(c chan accessToken, appid string, secret string) {
 	token := accessToken{"", time.Now()}
 	c <- token
 	for {
-		if time.Since(token.expires).Seconds() >= 0 {
+		swapped := atomic.CompareAndSwapInt32(&wx.refreshToken, 1, 0)
+		if swapped || time.Since(token.expires).Seconds() >= 0 {
 			var expires time.Duration
 			token.token, expires = authAccessToken(appid, secret)
 			token.expires = time.Now().Add(expires)
@@ -843,7 +874,6 @@ func sendGetRequest(reqURL string, c chan accessToken) ([]byte, error) {
 	for i := 0; i < retryMaxN; i++ {
 		token := <-c
 		if time.Since(token.expires).Seconds() < 0 {
-			log.Println("token:", token.token)
 			r, err := http.Get(reqURL + token.token)
 			if err != nil {
 				return nil, err
@@ -856,15 +886,14 @@ func sendGetRequest(reqURL string, c chan accessToken) ([]byte, error) {
 			var result response
 			if err := json.Unmarshal(reply, &result); err != nil {
 				return nil, err
-			} else {
-				switch result.ErrorCode {
-				case 0:
-					return reply, nil
-				case 42001: // access_token timeout and retry
-					continue
-				default:
-					return nil, errors.New(fmt.Sprintf("WeiXin send get request reply[%d]: %s", result.ErrorCode, result.ErrorMessage))
-				}
+			}
+			switch result.ErrorCode {
+			case 0:
+				return reply, nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return nil, errors.New(fmt.Sprintf("WeiXin send get request reply[%d]: %s", result.ErrorCode, result.ErrorMessage))
 			}
 		}
 	}
@@ -877,29 +906,24 @@ func postRequest(reqURL string, c chan accessToken, data []byte) ([]byte, error)
 		if time.Since(token.expires).Seconds() < 0 {
 			r, err := http.Post(reqURL+token.token, "application/json; charset=utf-8", bytes.NewReader(data))
 			if err != nil {
-				log.Println("err1", err.Error())
 				return nil, err
 			}
 			defer r.Body.Close()
 			reply, err := ioutil.ReadAll(r.Body)
-			log.Println("repl:", string(reply))
 			if err != nil {
-				log.Println("err2", err.Error())
 				return nil, err
 			}
 			var result response
 			if err := json.Unmarshal(reply, &result); err != nil {
-				log.Println("err3", err.Error())
 				return nil, err
-			} else {
-				switch result.ErrorCode {
-				case 0:
-					return reply, nil
-				case 42001: // access_token timeout and retry
-					continue
-				default:
-					return nil, errors.New(fmt.Sprintf("WeiXin send post request reply[%d]: %s", result.ErrorCode, result.ErrorMessage))
-				}
+			}
+			switch result.ErrorCode {
+			case 0:
+				return reply, nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return nil, errors.New(fmt.Sprintf("WeiXin send post request reply[%d]: %s", result.ErrorCode, result.ErrorMessage))
 			}
 		}
 	}
@@ -907,7 +931,7 @@ func postRequest(reqURL string, c chan accessToken, data []byte) ([]byte, error)
 }
 
 func postMessage(c chan accessToken, msg interface{}) error {
-	data, err := json.Marshal(msg)
+	data, err := marshal(msg)
 	if err != nil {
 		return err
 	}
@@ -949,15 +973,14 @@ func uploadMedia(c chan accessToken, mediaType string, filename string, reader i
 			err = json.Unmarshal(reply, &result)
 			if err != nil {
 				return "", err
-			} else {
-				switch result.ErrorCode {
-				case 0:
-					return result.MediaId, nil
-				case 42001: // access_token timeout and retry
-					continue
-				default:
-					return "", errors.New(fmt.Sprintf("WeiXin upload[%d]: %s", result.ErrorCode, result.ErrorMessage))
-				}
+			}
+			switch result.ErrorCode {
+			case 0:
+				return result.MediaId, nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return "", errors.New(fmt.Sprintf("WeiXin upload[%d]: %s", result.ErrorCode, result.ErrorMessage))
 			}
 		}
 	}
@@ -1024,11 +1047,6 @@ func (w responseWriter) ReplyText(text string) {
 	msg := fmt.Sprintf(replyText, w.replyHeader(), text)
 	w.writer.Write([]byte(msg))
 }
-func (w responseWriter) Reply2CustomerService() {
-	msg := fmt.Sprintf(reply2CustomerService, w.replyHeader(), "")
-	//log.Println("repl msg:", msg)
-	w.writer.Write([]byte(msg))
-}
 
 // Reply image message
 func (w responseWriter) ReplyImage(mediaId string) {
@@ -1070,6 +1088,12 @@ func (w responseWriter) TransferCustomerService(serviceId string) {
 	w.writer.Write([]byte(msg))
 }
 
+func (w responseWriter) TransferCustomerService2(serviceId, kfaccount string) {
+	msg := fmt.Sprintf(transferCustomerService2, serviceId, w.fromUserName, time.Now().Unix(), kfaccount)
+	log.Println("send custom msg:::", msg)
+	w.writer.Write([]byte(msg))
+}
+
 // Post text message
 func (w responseWriter) PostText(text string) error {
 	return w.wx.PostText(w.toUserName, text)
@@ -1101,7 +1125,7 @@ func (w responseWriter) PostNews(articles []Article) error {
 }
 
 // Post template message
-func (w responseWriter) PostTemplateMessage(templateid string, url string, data interface{}) (string, error) {
+func (w responseWriter) PostTemplateMessage(templateid string, url string, data TmplData) (int32, error) {
 	return w.wx.PostTemplateMessage(w.toUserName, templateid, url, data)
 }
 
@@ -1125,6 +1149,37 @@ func (w responseWriter) DownloadMedia(mediaId string, writer io.Writer) error {
 	return w.wx.DownloadMedia(mediaId, writer)
 }
 
+//
+func (wx *Weixin) PostCustomMsg(url string, obj interface{}) (bs []byte, err error) {
+	var data []byte
+	data, err = json.Marshal(obj)
+	if err != nil {
+		return
+	}
+	bs, err = postRequest(url, wx.tokenChan, data)
+	return
+}
+
+//
+func (wx *Weixin) GetKfList() (ret map[string]interface{}) {
+	bs, err := sendGetRequest("https://api.weixin.qq.com/cgi-bin/customservice/getkflist?access_token=", wx.tokenChan)
+	if err != nil {
+		return nil
+	}
+	json.Unmarshal(bs, &ret)
+	return
+}
+
+//
+func (wx *Weixin) GetOnlineKfList() (ret map[string]interface{}) {
+	bs, err := sendGetRequest("https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist?access_token=", wx.tokenChan)
+	if err != nil {
+		return nil
+	}
+	json.Unmarshal(bs, &ret)
+	return
+}
+
 func (rw responseWriter) GetUserBaseInfo(openid string) (map[string]interface{}, error) {
 	return rw.wx.GetUserBaseInfo(openid)
 }
@@ -1142,3 +1197,20 @@ func (w *Weixin) GetUserBaseInfo(openid string) (map[string]interface{}, error)
 		return nil, err
 	}
 }
+
+func (w responseWriter) Reply2CustomerService() {
+	msg := fmt.Sprintf(reply2CustomerService, w.replyHeader(), "")
+	//log.Println("repl msg:", msg)
+	w.writer.Write([]byte(msg))
+}
+
+//Post custom message(消息结构体完全由开发人员自定义)
+func (wx *Weixin) PostTextCustom(url string, obj interface{}) error {
+	data, err := json.Marshal(obj)
+	if err != nil {
+		return err
+	}
+	//log.Println("custom msg:", string(data))
+	_, err = postRequest(url, wx.tokenChan, data)
+	return err
+}

+ 2 - 0
weixin/src/qfw/weixinconfig/weixinconfig.go

@@ -39,6 +39,8 @@ type wxconfig struct {
 	Rpcserver             string                 `json:"rpcserver"`
 	Msgserver             string                 `json:"msgserver"`
 	Qmxcdn                string                 `json:"qmxcdn"`
+	Kfprefix              string                 `json:"kfprefix"`
+	Kfport                string                 `json:"kfport"`
 }
 
 //系统配置