Prechádzať zdrojové kódy

Merge branch 'release_1.3_20160104' of 192.168.3.17:zhanghongbo/qfw into release_1.3_20160104

renzheng 9 rokov pred
rodič
commit
2ad21383b3

+ 1 - 0
core/src/qfw/mobile/mobile.go

@@ -10,6 +10,7 @@ type Mobile struct {
 	search     xweb.Mapper `xweb:"/ent/(.*)/search"`
 	detail     xweb.Mapper `xweb:"/ent/(.*)/detail/(.*)/(.*)"`
 	pageerror  xweb.Mapper `xweb:"/ent/(.*)/505"`
+	guide      xweb.Mapper `xweb:"/swordfish/guide"`
 	wxrssset   xweb.Mapper `xweb:"/swordfish/page"`
 	msgSet     xweb.Mapper `xweb:"/swordfish/msgpushsetting/msgset"`
 	ajaxReq    xweb.Mapper `xweb:"/swordfish/ajaxReq"`

+ 12 - 0
core/src/qfw/mobile/wxmenu.go

@@ -18,6 +18,10 @@ func init() {
 	se = util.SimpleEncrypt{Key: "topnet"}
 }
 
+func (m *Mobile) Guide() error {
+	return m.Render("/swordfish/wxindex.html")
+}
+
 func (m *Mobile) Wxrssset() error {
 	defer func() {
 		if r := recover(); r != nil {
@@ -33,6 +37,14 @@ func (m *Mobile) Wxrssset() error {
 	}()
 	if m.Session().Get("userId") != nil {
 		userInfo := mongodb.FindById("user", m.GetSession("userId").(string), nil)
+		if i_m_guide := (*userInfo)["i_m_guide"]; util.IntAll(i_m_guide) == 0 {
+			mongodb.Update("user", `{"_id":"`+m.GetSession("userId").(string)+`"}`, map[string]interface{}{
+				"$set": map[string]interface{}{
+					"i_m_guide": 1,
+				},
+			}, false, false)
+			return m.Redirect("/swordfish/guide")
+		}
 		m.T["msgset"] = (*userInfo)["o_msgset"]
 		entid := util.ObjToString((*userInfo)["s_enterpriseid"])
 		if entid != "" {

+ 1 - 1
core/src/web/staticres/css/swordfish.css

@@ -216,7 +216,7 @@ a:focus, a:hover{
 .swordfish-right{
 	padding-right: 0px;
 	background-color: #FFFFFF;
-	max-width: 250px;
+	width: 250px;
 	display: inline-block;
 	vertical-align: top;
 	float: right;

BIN
core/src/web/staticres/images/swordfish/guide-1.png


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 14
core/src/web/staticres/microwebsite/swiper/swiper.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5 - 0
core/src/web/staticres/swiper/animate.min.css


+ 0 - 0
core/src/web/staticres/microwebsite/swiper/maps/swiper.jquery.min.js.map → core/src/web/staticres/swiper/maps/swiper.jquery.min.js.map


+ 0 - 0
core/src/web/staticres/microwebsite/swiper/maps/swiper.min.js.map → core/src/web/staticres/swiper/maps/swiper.min.js.map


+ 2 - 0
core/src/web/staticres/swiper/swiper.animate.min.js

@@ -0,0 +1,2 @@
+//本插件由www.swiper.com.cn提供
+function swiperAnimateCache(){for(allBoxes=window.document.documentElement.querySelectorAll(".ani"),i=0;i<allBoxes.length;i++)allBoxes[i].attributes["style"]?allBoxes[i].setAttribute("swiper-animate-style-cache",allBoxes[i].attributes["style"].value):allBoxes[i].setAttribute("swiper-animate-style-cache"," "),allBoxes[i].style.visibility="hidden"}function swiperAnimate(a){clearSwiperAnimate();var b=a.slides[a.activeIndex].querySelectorAll(".ani");for(i=0;i<b.length;i++)b[i].style.visibility="visible",effect=b[i].attributes["swiper-animate-effect"]?b[i].attributes["swiper-animate-effect"].value:"",b[i].className=b[i].className+"  "+effect+" "+"animated",style=b[i].attributes["style"].value,duration=b[i].attributes["swiper-animate-duration"]?b[i].attributes["swiper-animate-duration"].value:"",duration&&(style=style+"animation-duration:"+duration+";-webkit-animation-duration:"+duration+";"),delay=b[i].attributes["swiper-animate-delay"]?b[i].attributes["swiper-animate-delay"].value:"",delay&&(style=style+"animation-delay:"+delay+";-webkit-animation-delay:"+delay+";"),b[i].setAttribute("style",style)}function clearSwiperAnimate(){for(allBoxes=window.document.documentElement.querySelectorAll(".ani"),i=0;i<allBoxes.length;i++)allBoxes[i].attributes["swiper-animate-style-cache"]&&allBoxes[i].setAttribute("style",allBoxes[i].attributes["swiper-animate-style-cache"].value),allBoxes[i].style.visibility="hidden",allBoxes[i].className=allBoxes[i].className.replace("animated"," "),allBoxes[i].attributes["swiper-animate-effect"]&&(effect=allBoxes[i].attributes["swiper-animate-effect"].value,allBoxes[i].className=allBoxes[i].className.replace(effect," "))}

+ 0 - 0
core/src/web/staticres/microwebsite/swiper/swiper.min.css → core/src/web/staticres/swiper/swiper.min.css


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 14 - 0
core/src/web/staticres/swiper/swiper.min.js


BIN
core/src/web/staticres/wxrssset/images/up.png


+ 3 - 0
core/src/web/staticres/wxrssset/main.js

@@ -83,6 +83,9 @@ KeyWordDialog.AppendNode = function(dialogObj,value){
 			$(this).children("lable").children("font").text(i+1);
 		});
 	});
+	if(typeof(value) == "undefined" || value == ""){
+		obj.find("[type='text']").focus()
+	}
 }
 //信息范围
 function ScopeDialog(type,clickLi){

+ 147 - 0
core/src/web/staticres/wxrssset/style.css

@@ -279,4 +279,151 @@ img{
 	color: #ccc;
 	text-align: right;
 	padding-right: 5px;
+}
+/**引导页**/
+.swiper-pagination{
+	top: 80px !important;
+}
+.swiper-pagination-bullet{
+	margin-bottom: 15px !important;
+}
+.swiper-slide{
+	text-align: center;
+	/*z-index: -1;
+	background: #ffffff;
+	position: absolute !important;*/
+}
+.swiper-slide-active{
+	/*z-index: 0;*/
+}
+.slide-down{
+	z-index: 1;
+	/* Safari and Chrome: */
+	-webkit-animation-name:slidedown;
+	-webkit-animation-duration:2s;
+	-webkit-animation-timing-function:ease-in-out;
+	-webkit-animation-iteration-count:1;
+	-webkit-animation-play-state:running;
+}
+@-webkit-keyframes slidedown {
+	0%{
+		top: -100%;
+	}
+	100% {
+		top: 0px;
+	}
+}
+.slide-up{
+	z-index: 1;
+	/* Safari and Chrome: */
+	-webkit-animation-name:slideup;
+	-webkit-animation-duration:2s;
+	-webkit-animation-timing-function:ease-in-out;
+	-webkit-animation-iteration-count:1;
+	-webkit-animation-play-state:running;
+}
+
+@-webkit-keyframes slideup {
+	0%{
+		top: 100%;
+	}
+	100% {
+		top: 0px;
+	}
+}
+.swiper-slide>p{
+	height: inherit;
+	position: relative;
+}
+.swiper-slide img{
+	position: relative;
+	top: 50%;
+	margin-top: -302px;
+}
+.swiper-pagination-bullet-active{
+	background-color: #37C6DA;
+}
+.guide-bottom{
+	position: absolute;
+	bottom: 10px;
+	text-align: center;
+	width: 100%;
+	z-index: 99999;
+}
+.guide-bottom img{
+	width: 25px;
+	height: 15px;
+	position: absolute;
+	left: 50%;
+	margin-left: -25px;
+	animation-name:start;
+	animation-duration:2s;
+	animation-timing-function:ease-in-out;
+	animation-iteration-count:infinite;
+	animation-play-state:running;
+	/* Firefox: */
+	-moz-animation-name:start;
+	-moz-animation-duration:2s;
+	-moz-animation-timing-function:ease-in-out;
+	-moz-animation-iteration-count:infinite;
+	-moz-animation-play-state:running;
+	/* Safari and Chrome: */
+	-webkit-animation-name:start;
+	-webkit-animation-duration:2s;
+	-webkit-animation-timing-function:ease-in-out;
+	-webkit-animation-iteration-count:infinite;
+	-webkit-animation-play-state:running;
+	/* Opera: */
+	-o-animation-name:start;
+	-o-animation-duration:2s;
+	-o-animation-timing-function:ease-in-out;
+	-o-animation-iteration-count:infinite;
+	-o-animation-play-state:running;
+}
+
+@-webkit-keyframes start {
+	0%{
+		opacity:0;
+		bottom: 0px;
+	}
+	60% {
+		opacity:1;
+	}
+	100% {
+		opacity:0;
+		bottom: 25px;
+	}
+}
+@-moz-keyframes start {
+	0%{
+		opacity:0;
+		bottom: 0px;
+	}
+	60% {
+		opacity:1;
+	}
+	100% {
+		opacity:0;
+		bottom: 25px;
+	}
+}
+@keyframes start {
+		0% {
+		opacity:0;
+		bottom: 0px;
+	}
+	60% {
+		opacity:1;
+	}
+	100% {
+		opacity:0;
+		bottom: 25px;
+	}
+}
+.guide-bottom span{
+	float: right;
+	display: block;
+	font-size: 12px;
+	color: #D7D7D7;
+	margin-right: 10px;
 }

+ 2 - 2
core/src/web/templates/microwebsite/blue/index.html

@@ -4,11 +4,11 @@
 <meta content="telephone=no" name="format-detection">
 <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
 <title>{{.T.EntName}}</title>
-<link rel="stylesheet" href="/microwebsite/swiper/swiper.min.css">
+<link rel="stylesheet" href="/swiper/swiper.min.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/style.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/blue.css">
 <script type="text/javascript" src="/js/jquery.js"></script>
-<script type="text/javascript" src="/microwebsite/swiper/swiper.min.js"></script>
+<script type="text/javascript" src="/swiper/swiper.min.js"></script>
 <script type="text/javascript" src="/microwebsite/mobile/js/index.js"></script>
 <script type="text/javascript">
 $(function(){

+ 2 - 2
core/src/web/templates/microwebsite/colorful/index.html

@@ -4,11 +4,11 @@
 <meta content="telephone=no" name="format-detection">
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 <title>{{.T.EntName}}</title>
-<link rel="stylesheet" href="/microwebsite/swiper/swiper.min.css">
+<link rel="stylesheet" href="/swiper/swiper.min.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/style.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/colorful.css">
 <script type="text/javascript" src="/js/jquery.js"></script>
-<script type="text/javascript" src="/microwebsite/swiper/swiper.min.js"></script>
+<script type="text/javascript" src="/swiper/swiper.min.js"></script>
 <script type="text/javascript" src="/microwebsite/mobile/js/index.js"></script>
 </head>
 <body class="content-bg">

+ 2 - 2
core/src/web/templates/microwebsite/geenleaf/index.html

@@ -4,11 +4,11 @@
 <meta content="telephone=no" name="format-detection">
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 <title>{{.T.EntName}}</title>
-<link rel="stylesheet" href="/microwebsite/swiper/swiper.min.css">
+<link rel="stylesheet" href="/swiper/swiper.min.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/style.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/geenleaf.css">
 <script type="text/javascript" src="/js/jquery.js"></script>
-<script type="text/javascript" src="/microwebsite/swiper/swiper.min.js"></script>
+<script type="text/javascript" src="/swiper/swiper.min.js"></script>
 <script type="text/javascript" src="/microwebsite/mobile/js/index.js"></script>
 </head>
 <body class="content-bg">

+ 2 - 2
core/src/web/templates/microwebsite/universe/index.html

@@ -4,11 +4,11 @@
 <meta content="telephone=no" name="format-detection">
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 <title>{{.T.EntName}}</title>
-<link rel="stylesheet" href="/microwebsite/swiper/swiper.min.css">
+<link rel="stylesheet" href="/swiper/swiper.min.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/style.css">
 <link rel="stylesheet" href="/microwebsite/mobile/css/universe.css">
 <script type="text/javascript" src="/js/jquery.js"></script>
-<script type="text/javascript" src="/microwebsite/swiper/swiper.min.js"></script>
+<script type="text/javascript" src="/swiper/swiper.min.js"></script>
 <script type="text/javascript" src="/microwebsite/mobile/js/index.js"></script>
 </head>
 <body class="content-bg">

+ 3 - 0
core/src/web/templates/swordfish/rssset.html

@@ -377,6 +377,9 @@ function appendKeyWord(type,value){
 		autoChecked(type,false);
 		ajaxReq();
 	});
+	if(typeof(value) == "undefined" || value == ""){
+		node.children("[type='text']").focus()
+	}
 	autoChecked(type,false);
 }
 function ajaxReq(){

+ 90 - 0
core/src/web/templates/swordfish/wxindex.html

@@ -0,0 +1,90 @@
+<html>
+<head>
+<title>剑鱼</title>
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+<link href="/wxrssset/style.css" rel="stylesheet">
+<link href="/swiper/swiper.min.css" rel="stylesheet">
+<link href="/swiper/animate.min.css" rel="stylesheet">
+<script src="/js/jquery.js"></script>
+<script src="/swiper/swiper.min.js"></script>
+<script src="/swiper/swiper.animate.min.js"></script>
+</head>
+<body>
+<div class="swiper-container">
+    <div class="swiper-wrapper">
+        <div class="swiper-slide">
+				<img id="guide-firstImg" src="/images/swordfish/guide-1.png">
+		</div>
+       	<div class="swiper-slide">
+				<img src="/images/swordfish/guide-2.png">
+		</div>
+       	<div class="swiper-slide">
+				<img src="/images/swordfish/guide-3.png">
+		</div>
+		<div class="swiper-slide">
+				<img src="/images/swordfish/guide-4.png">
+		</div>
+		<div class="swiper-slide">
+				<img src="/images/swordfish/guide-5.png">
+		</div>
+    </div>
+    <!-- 如果需要分页器 -->
+   	<div class="swiper-pagination"></div>
+	<div class="guide-bottom">
+		<img src="/wxrssset/images/up.png">
+		<span onclick="window.location.href='/swordfish/page'">跳过引导>></span>
+	</div>
+</div>
+<script type="text/javascript">
+$(function(){
+	var currentIndex = 0;
+	var mySwiper = new Swiper('.swiper-container', {
+        pagination: '.swiper-pagination',
+        paginationClickable: true,
+		loop: true,
+        direction: 'vertical',
+		touchMoveStopPropagation: false,
+		virtualTranslate: false,
+		onSlideNextStart: function(swiper){
+			
+		},
+		onSlidePrevEnd: function(swiper){
+		},
+		onInit: function(swiper){ //Swiper2.x的初始化是onFirstInit
+		    //swiperAnimateCache(swiper); //隐藏动画元素 
+		    //swiperAnimate(swiper); //初始化完成开始动画
+			
+		}, 
+		onSlideChangeStart: function(swiper){
+			/*console.info(currentIndex+"--------"+swiper.activeIndex);
+			if(currentIndex > swiper.activeIndex){
+				 $(swiper.slides[swiper.activeIndex]).addClass("slide-down");
+			}else if(currentIndex < swiper.activeIndex){
+				 $(swiper.slides[swiper.activeIndex]).addClass("slide-up");
+			}
+			currentIndex = swiper.activeIndex-1;
+			$(swiper.slides[currentIndex]).one("webkitAnimationEnd",function(){
+				//$(this).removeClass("slide-up").removeClass("slide-down");
+			});*/
+		},
+	  	onSlideChangeEnd: function(swiper){ 
+	    	//swiperAnimate(swiper); //每个slide切换结束时也运行当前slide动画
+	  	},
+		onSliderMove: function(swiper, event){
+			
+		}
+    });
+	var imgHeight = 604;
+	var imgWidth = 813;
+	var width = document.body.clientWidth;
+	var height = document.body.clientHeight;
+	if(imgWidth > width){
+		var h = width / imgWidth * imgHeight;
+		$(".swiper-slide img").css({width: width,height: h,marginTop: -(h / 2)});
+	}else if(imgHeight > height){
+		$(".swiper-slide img").css({width: height / imgHeight * imgWidth,height: height,marginTop: -(height / 2)});
+	}
+});
+</script>
+</body>
+</html>

+ 21 - 0
weixin/src/qfw/weixin/rpc/jssdk.go

@@ -0,0 +1,21 @@
+package rpc
+
+//JS 微信接口
+import (
+	"fmt"
+	"qfw/util"
+	wf "qfw/weixinconfig"
+	"time"
+)
+
+//取得JS接口参数
+func (wxrpc *WeiXinRpc) GetJSInterfaceParam(currenturl /*当前页面的URL一定要真实*/ string, ret *[]string) (err error) {
+	timestamp := time.Now().Unix()
+	noncestr := util.Uuid(32)
+	signature, _ := wxrpc.wx.JsSignature(currenturl, timestamp, noncestr)
+	*ret = append(*ret, wf.SysConfig.Appid)
+	*ret = append(*ret, fmt.Sprintf("%d", timestamp))
+	*ret = append(*ret, noncestr)
+	*ret = append(*ret, signature)
+	return nil
+}

+ 343 - 61
weixin/src/qfw/weixin/weixinsdk.go

@@ -13,6 +13,7 @@ import (
 	"log"
 	"mime/multipart"
 	"net/http"
+	"net/url"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -23,45 +24,71 @@ import (
 
 const (
 	// Event type
-	msgEvent         = "event"
-	EventSubscribe   = "subscribe"
-	EventUnsubscribe = "unsubscribe"
-	EventScan        = "scan"
-	EventClick       = "CLICK"
+	msgEvent          = "event"
+	EventSubscribe    = "subscribe"
+	EventUnsubscribe  = "unsubscribe"
+	EventScan         = "scan"
+	EventView         = "VIEW"
+	EventClick        = "CLICK"
+	EventLocation     = "LOCATION"
+	EventTemplateSent = "TEMPLATESENDJOBFINISH"
+
 	// Message type
-	MsgTypeDefault          = ".*"
-	MsgTypeText             = "text"
-	MsgTypeImage            = "image"
-	MsgTypeVoice            = "voice"
-	MsgTypeVideo            = "video"
-	MsgTypeLocation         = "location"
-	MsgTypeLink             = "link"
-	MsgTypeEvent            = msgEvent + ".*"
-	MsgTypeEventSubscribe   = msgEvent + "\\." + EventSubscribe
-	MsgTypeEventUnsubscribe = msgEvent + "\\." + EventUnsubscribe
-	MsgTypeEventScan        = msgEvent + "\\." + EventScan
-	MsgTypeEventClick       = msgEvent + "\\." + EventClick
+	MsgTypeDefault           = ".*"
+	MsgTypeText              = "text"
+	MsgTypeImage             = "image"
+	MsgTypeVoice             = "voice"
+	MsgTypeVideo             = "video"
+	MsgTypeShortVideo        = "shortvideo"
+	MsgTypeLocation          = "location"
+	MsgTypeLink              = "link"
+	MsgTypeEvent             = msgEvent + ".*"
+	MsgTypeEventSubscribe    = msgEvent + "\\." + EventSubscribe
+	MsgTypeEventUnsubscribe  = msgEvent + "\\." + EventUnsubscribe
+	MsgTypeEventScan         = msgEvent + "\\." + EventScan
+	MsgTypeEventView         = msgEvent + "\\." + EventView
+	MsgTypeEventClick        = msgEvent + "\\." + EventClick
+	MsgTypeEventLocation     = msgEvent + "\\." + EventLocation
+	MsgTypeEventTemplateSent = msgEvent + "\\." + EventTemplateSent
+
 	// Media type
 	MediaTypeImage = "image"
 	MediaTypeVoice = "voice"
 	MediaTypeVideo = "video"
 	MediaTypeThumb = "thumb"
 	// Button type
-	MenuButtonTypeKey        = "click"
-	MenuButtonTypeUrl        = "view"
-	MenuButtonTypeMedia      = "media_id"
-	MenuButtonTypePicOrAlbum = "pic_photo_or_album"
+	MenuButtonTypeKey             = "click"
+	MenuButtonTypeUrl             = "view"
+	MenuButtonTypeScancodePush    = "scancode_push"
+	MenuButtonTypeScancodeWaitmsg = "scancode_waitmsg"
+	MenuButtonTypePicSysphoto     = "pic_sysphoto"
+	MenuButtonTypePicPhotoOrAlbum = "pic_photo_or_album"
+	MenuButtonTypePicWeixin       = "pic_weixin"
+	MenuButtonTypeLocationSelect  = "location_select"
+	MenuButtonTypeMediaId         = "media_id"
+	MenuButtonTypeViewLimited     = "view_limited"
+	// Template Status
+	TemplateSentStatusSuccess      = "success"
+	TemplateSentStatusUserBlock    = "failed:user block"
+	TemplateSentStatusSystemFailed = "failed:system failed"
+	// Redirect Scope
+	RedirectURLScopeBasic    = "snsapi_base"
+	RedirectURLScopeUserInfo = "snsapi_userinfo"
 	// Weixin host URL
-	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"
-	weixinFileURL     = "http://file.api.weixin.qq.com/cgi-bin/media"
-	weixinUserInfo    = "https://api.weixin.qq.com/cgi-bin/user/info"
+	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"
+	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"
+	weixinTemplate           = "https://api.weixin.qq.com/cgi-bin/template"
+	weixinRedirectURL        = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
+	weixinUserAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
+	weixinJsApiTicketURL     = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
 	// Max retry count
 	retryMaxN = 3
 	// Reply format
 	replyText               = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
-	reply2CustomerService   = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></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>"
@@ -70,10 +97,11 @@ const (
 	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>"
 
 	// 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}}}"
+	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}}}`
 )
 
 // Common message header
@@ -107,6 +135,7 @@ type Request struct {
 	Longitude    float32
 	Precision    float32
 	Recognition  string
+	Status       string
 }
 
 // Use to reply music message
@@ -142,10 +171,35 @@ type MenuButton struct {
 	Type       string       `json:"type,omitempty"`
 	Key        string       `json:"key,omitempty"`
 	Url        string       `json:"url,omitempty"`
-	Mediaid    int          `json:"media_id"`
+	MediaId    string       `json:"media_id,omitempty"`
 	SubButtons []MenuButton `json:"sub_button,omitempty"`
 }
 
+type UserAccessToken struct {
+	AccessToken   string `json:"access_token"`
+	RefreshToken  string `json:"refresh_token"`
+	ExpireSeconds int    `json:"expires_in"`
+	OpenId        string `json:"openid"`
+	Scope         string `json:"scope"`
+	UnionId       string `json:"unionid,omitempty"`
+}
+
+type UserInfo struct {
+	Subscribe     int    `json:"subscribe,omitempty"`
+	Language      string `json:"language,omitempty"`
+	OpenId        string `json:"openid,omitempty"`
+	UnionId       string `json:"unionid,omitempty"`
+	Nickname      string `json:"nickname,omitempty"`
+	Sex           int    `json:"sex,omitempty"`
+	City          string `json:"city,omitempty"`
+	Country       string `json:"country,omitempty"`
+	Province      string `json:"province,omitempty"`
+	HeadImageUrl  string `json:"headimgurl,omitempty"`
+	SubscribeTime int64  `json:"subscribe_time,omitempty"`
+	Remark        string `json:"remark,omitempty"`
+	GroupId       int    `json:"groupid,omitempty"`
+}
+
 // Use to output reply
 type ResponseWriter interface {
 	// Get weixin
@@ -168,6 +222,7 @@ 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)
 	// Media operator
 	UploadMediaFromFile(mediaType string, filepath string) (string, error)
 	DownloadMediaToFile(mediaId string, filepath string) error
@@ -201,11 +256,19 @@ type accessToken struct {
 	expires time.Time
 }
 
+type jsApiTicket struct {
+	ticket  string
+	expires time.Time
+}
+
 type Weixin struct {
-	token     string
-	routes    []*route
-	tokenChan chan accessToken
-	userData  interface{}
+	token      string
+	routes     []*route
+	tokenChan  chan accessToken
+	ticketChan chan jsApiTicket
+	userData   interface{}
+	appId      string
+	appSecret  string
 }
 
 //
@@ -219,9 +282,13 @@ func (qr *QRScene) ToURL() string {
 func New(token string, appid string, secret string) *Weixin {
 	wx := &Weixin{}
 	wx.token = token
+	wx.appId = appid
+	wx.appSecret = secret
 	if len(appid) > 0 && len(secret) > 0 {
 		wx.tokenChan = make(chan accessToken)
 		go createAccessToken(wx.tokenChan, appid, secret)
+		wx.ticketChan = make(chan jsApiTicket)
+		go createJsApiTicket(wx.tokenChan, wx.ticketChan)
 	}
 	return wx
 }
@@ -232,6 +299,14 @@ func NewWithUserData(token string, appid string, secret string, userData interfa
 	return wx
 }
 
+func (wx *Weixin) GetAppId() string {
+	return wx.appId
+}
+
+func (wx *Weixin) GetAppSecret() string {
+	return wx.appSecret
+}
+
 // Register request callback.
 func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
 	regex, err := regexp.Compile(pattern)
@@ -387,6 +462,21 @@ func (wx *Weixin) DownloadMedia(mediaId string, writer io.Writer) error {
 	return downloadMedia(wx.tokenChan, mediaId, writer)
 }
 
+// Get ip list
+func (wx *Weixin) GetIpList() ([]string, error) {
+	reply, err := sendGetRequest(weixinHost+"/getcallbackip?access_token=", wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result struct {
+		IpList []string `json:"ip_list"`
+	}
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return result.IpList, nil
+}
+
 // Create QR scene
 func (wx *Weixin) CreateQRScene(sceneId int, expires int) (*QRScene, error) {
 	reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRScene, expires, sceneId)))
@@ -413,6 +503,31 @@ func (wx *Weixin) CreateQRLimitScene(sceneId int) (*QRScene, error) {
 	return &qr, nil
 }
 
+// Long url to short url
+func (wx *Weixin) ShortURL(url string) (string, error) {
+	var request struct {
+		Action  string `json:"action"`
+		LongUrl string `json:"long_url"`
+	}
+	request.Action = "long2short"
+	request.LongUrl = url
+	data, err := marshal(request)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinShortURL+"?access_token=", wx.tokenChan, data)
+	if err != nil {
+		return "", err
+	}
+	var shortUrl struct {
+		Url string `json:"short_url"`
+	}
+	if err := json.Unmarshal(reply, &shortUrl); err != nil {
+		return "", err
+	}
+	return shortUrl.Url, nil
+}
+
 // Custom menu
 func (wx *Weixin) CreateMenu(menu *Menu) error {
 	data, err := json.Marshal(menu)
@@ -431,16 +546,14 @@ func (wx *Weixin) GetMenu() (*Menu, error) {
 	reply, err := sendGetRequest(weixinHost+"/menu/get?access_token=", wx.tokenChan)
 	if err != nil {
 		return nil, err
-	} else {
-		var result struct {
-			MenuCtx *Menu `json:"menu"`
-		}
-		if err := json.Unmarshal(reply, &result); err != nil {
-			return nil, err
-		} else {
-			return result.MenuCtx, nil
-		}
 	}
+	var result struct {
+		MenuCtx *Menu `json:"menu"`
+	}
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return result.MenuCtx, nil
 }
 
 func (wx *Weixin) DeleteMenu() error {
@@ -448,6 +561,129 @@ func (wx *Weixin) DeleteMenu() error {
 	return err
 }
 
+// Template
+func (wx *Weixin) SetTemplateIndustry(id1 string, id2 string) error {
+	var industry struct {
+		Id1 string `json:"industry_id1,omitempty"`
+		Id2 string `json:"industry_id2,omitempty"`
+	}
+	industry.Id1 = id1
+	industry.Id2 = id2
+	data, err := marshal(industry)
+	if err != nil {
+		return err
+	}
+	_, err = postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
+	return err
+}
+
+func (wx *Weixin) AddTemplate(shortid string) (string, error) {
+	var request struct {
+		Shortid string `json:"template_id_short,omitempty"`
+	}
+	request.Shortid = shortid
+	data, err := marshal(request)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
+	if err != nil {
+		return "", err
+	}
+	var templateId struct {
+		Id string `json:"template_id,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &templateId); err != nil {
+		return "", err
+	}
+	return templateId.Id, nil
+}
+
+func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data interface{}) (string, error) {
+	var msg struct {
+		ToUser     string      `json:"touser"`
+		TemplateId string      `json:"template_id"`
+		Url        string      `json:"url,omitempty"`
+		Data       interface{} `json:"data,omitempty"`
+	}
+	msg.ToUser = touser
+	msg.TemplateId = templateid
+	msg.Url = url
+	msg.Data = data
+	msgStr, err := marshal(msg)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
+	if err != nil {
+		return "", err
+	}
+	var resp struct {
+		MsgId string `json:"msgid,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &resp); err != nil {
+		return "", err
+	}
+	return resp.MsgId, nil
+}
+
+// Create redirect url
+func (wx *Weixin) CreateRedirectURL(urlStr string, scope string, state string) string {
+	return fmt.Sprintf(weixinRedirectURL, wx.appId, url.QueryEscape(urlStr), scope, state)
+}
+
+// Get open id
+func (wx *Weixin) GetUserAccessToken(code string) (*UserAccessToken, error) {
+	resp, err := http.Get(fmt.Sprintf(weixinUserAccessTokenURL, wx.appId, wx.appSecret, code))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	var res UserAccessToken
+	if err := json.Unmarshal(body, &res); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
+// Get user info
+func (wx *Weixin) GetUserInfo(openid string) (*UserInfo, error) {
+	reply, err := sendGetRequest(fmt.Sprintf("%s?openid=%s&lang=zh_CN&access_token=", weixinUserInfo, openid), wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result UserInfo
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+func (wx *Weixin) GetJsApiTicket() (string, error) {
+	for i := 0; i < retryMaxN; i++ {
+		ticket := <-wx.ticketChan
+		if time.Since(ticket.expires).Seconds() < 0 {
+			return ticket.ticket, nil
+		}
+	}
+	return "", errors.New("Get JsApi Ticket Timeout")
+}
+
+func (wx *Weixin) JsSignature(url string, timestamp int64, noncestr string) (string, error) {
+	ticket, err := wx.GetJsApiTicket()
+	if err != nil {
+		return "", err
+	}
+	h := sha1.New()
+	h.Write([]byte(fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s",
+		ticket, noncestr, timestamp, url)))
+	return fmt.Sprintf("%x", h.Sum(nil)), nil
+}
+
 // Create handler func
 func (wx *Weixin) CreateHandlerFunc(w http.ResponseWriter, r *http.Request) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
@@ -505,6 +741,16 @@ func (wx *Weixin) routeRequest(w http.ResponseWriter, r *Request) {
 	return
 }
 
+func marshal(v interface{}) ([]byte, error) {
+	data, err := json.Marshal(v)
+	if err == nil {
+		data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
+		data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
+		data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
+	}
+	return data, err
+}
+
 func checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
 	r.ParseForm()
 	var signature string = r.FormValue("signature")
@@ -547,6 +793,25 @@ func authAccessToken(appid string, secret string) (string, time.Duration) {
 	return "", 0
 }
 
+func getJsApiTicket(c chan accessToken) (*jsApiTicket, error) {
+	reply, err := sendGetRequest(weixinJsApiTicketURL+"?type=jsapi&access_token=", c)
+	if err != nil {
+		return nil, err
+	}
+	var res struct {
+		Ticket    string `json:"ticket"`
+		ExpiresIn int64  `json:"expires_in"`
+	}
+	if err := json.Unmarshal(reply, &res); err != nil {
+		return nil, err
+	}
+	var ticket jsApiTicket
+	ticket.ticket = res.Ticket
+	ticket.expires = time.Now().Add(time.Duration(res.ExpiresIn * 1000 * 1000 * 1000))
+	return &ticket, nil
+
+}
+
 func createAccessToken(c chan accessToken, appid string, secret string) {
 	token := accessToken{"", time.Now()}
 	c <- token
@@ -560,6 +825,20 @@ func createAccessToken(c chan accessToken, appid string, secret string) {
 	}
 }
 
+func createJsApiTicket(cin chan accessToken, c chan jsApiTicket) {
+	ticket := jsApiTicket{"", time.Now()}
+	c <- ticket
+	for {
+		if time.Since(ticket.expires).Seconds() >= 0 {
+			t, err := getJsApiTicket(cin)
+			if err == nil {
+				ticket = *t
+			}
+		}
+		c <- ticket
+	}
+}
+
 func sendGetRequest(reqURL string, c chan accessToken) ([]byte, error) {
 	for i := 0; i < retryMaxN; i++ {
 		token := <-c
@@ -698,24 +977,22 @@ func downloadMedia(c chan accessToken, mediaId string, writer io.Writer) error {
 			if r.Header.Get("Content-Type") != "text/plain" {
 				_, err := io.Copy(writer, r.Body)
 				return err
-			} else {
-				reply, err := ioutil.ReadAll(r.Body)
-				if err != nil {
-					return err
-				}
-				var result response
-				if err := json.Unmarshal(reply, &result); err != nil {
-					return err
-				} else {
-					switch result.ErrorCode {
-					case 0:
-						return nil
-					case 42001: // access_token timeout and retry
-						continue
-					default:
-						return errors.New(fmt.Sprintf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage))
-					}
-				}
+			}
+			reply, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				return err
+			}
+			var result response
+			if err := json.Unmarshal(reply, &result); err != nil {
+				return err
+			}
+			switch result.ErrorCode {
+			case 0:
+				return nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return errors.New(fmt.Sprintf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage))
 			}
 		}
 	}
@@ -823,6 +1100,11 @@ func (w responseWriter) PostNews(articles []Article) error {
 	return w.wx.PostNews(w.toUserName, articles)
 }
 
+// Post template message
+func (w responseWriter) PostTemplateMessage(templateid string, url string, data interface{}) (string, error) {
+	return w.wx.PostTemplateMessage(w.toUserName, templateid, url, data)
+}
+
 // Upload media from local file
 func (w responseWriter) UploadMediaFromFile(mediaType string, filepath string) (string, error) {
 	return w.wx.UploadMediaFromFile(mediaType, filepath)

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov