微信公众号(服务号)开发-代码编写

本人使用java开发

框架:springboot 1.5.7.RELEASE

本次项目所用的服务器已有其他项目在运行,并且占用了443端口。原项目使用nginx实现转发,所以需要重新配置nginx增加域名转发,以满足微信必须使用443端口要求。
原nginx配置文件如下

user  www www;
worker_processes auto;
error_log  /www/wwwlogs/nginx_error.log  crit;
pid        /www/server/nginx/logs/nginx.pid;
worker_rlimit_nofile 51200;

events
    {
        use epoll;
        worker_connections 51200;
        multi_accept on;
    }

http
    {
        include       mime.types;
    include proxy.conf;
    #include luawaf.conf;

        default_type  application/octet-stream;

        server_names_hash_bucket_size 512;
        client_header_buffer_size 32k;
        large_client_header_buffers 4 32k;
        client_max_body_size 50m;

        sendfile   on;
        tcp_nopush on;

        keepalive_timeout 60;

        tcp_nodelay on;

        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 64k;
        fastcgi_buffers 4 64k;
        fastcgi_busy_buffers_size 128k;
        fastcgi_temp_file_write_size 256k;
        fastcgi_intercept_errors on;

        gzip on;
        gzip_min_length  1k;
        gzip_buffers     4 16k;
        gzip_http_version 1.1;
        gzip_comp_level 2;
        gzip_types     text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
        gzip_vary on;
        gzip_proxied   expired no-cache no-store private auth;
        gzip_disable   "MSIE [1-6]\.";

        limit_conn_zone $binary_remote_addr zone=perip:10m;
        limit_conn_zone $server_name zone=perserver:10m;

        server_tokens off;
        access_log off;

server
    {
        listen 888;
        server_name www.bt.cn;
        index index.html index.htm index.php;
        root  /www/server/phpmyadmin;

        #error_page   404   /404.html;
        include enable-php.conf;

        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires      30d;
        }

        location ~ .*\.(js|css)?$
        {
            expires      12h;
        }

        location ~ /\.
        {
            deny all;
        }

        access_log  /www/wwwlogs/access.log;
    }
include /www/server/panel/vhost/nginx/*.conf;
}

\color{red}{注意:}配置文件最下面 include 这行内容,会将*.conf文件全部引入到配置中。所以我们再 /www/server/panel/vhost/nginx/ 路径下新增本次项目所需配置,如下

server
{
    listen 80;
    listen 443 ssl;
    server_name 项目域名;
    root /根路径;
    
    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;
    #HTTP_TO_HTTPS_START
    if ($server_port !~ 443){
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END
    ssl_certificate    /ssl证书.crt;
    ssl_certificate_key    /ssl证书.rsa;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    error_page 497  https://$host$request_uri;

    #SSL-END
    
    #REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
    #REWRITE-END
    
    location = /xxx.txt{
        
    }
    
    location / {
                    proxy_pass https://项目域名:项目端口;
            }
    
    access_log  /www/wwwlogs/项目域名.log;
    error_log  /www/wwwlogs/项目域名.error.log;
}

配置文件中的 \color{red}{项目域名、根路径、ssl证书、项目端口} 需要替换成自己的内容。
location中的xxx.txt 是在上一篇中说的配置授权域名时的校验文件,将校验文件放置根目录下既可以访问到。

1.获取openid

本次项目集成了微信支付、公众号模板消息,需要使用到用户openid,所以首先获取openid。
配置公众号菜单 打开 https://项目域名/open/openid 地址。
该接口重定向至微信授权地址,只需要openid所以使用用户无感的 scope=snsapi_base 即可。
代码如下

@Controller
@Slf4j
public class LoginUserController {

    @Resource
    private ICourseService courseService;

    @Resource
    private MyWXPayConfig myWXPayConfig;

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private IWxAccessTokerService accessTokerService;

    @Resource
    private CourseLocationMapper locationMapper;

    @Autowired
    private IDataDictionaryService dataDictionaryService;

    @Value("${server.port}")
    private String port;

    @Value("${wxconfig.domain}")
    private String domain;

    @RequestMapping("/open/openid")
    public void getOpenId(HttpServletRequest request, HttpServletResponse response){
        StringBuffer sb = new StringBuffer();
        StringBuffer encodeUrl = new StringBuffer("https://");
        //公众号中配置的回调域名(网页授权回调域名)
        String root = request.getContextPath();
        String appId = myWXPayConfig.getAppID();

        sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
        sb.append(appId);
        try {
            //对重定向url进行编码,官方文档要求
            encodeUrl.append(domain).append(root)/*.append(":").append(port)*/;

            String url = URLEncoder.encode(encodeUrl.toString(), "utf-8");
            sb.append("&redirect_uri=").append(url);

            //网页授权的静默授权snsapi_base
            sb.append("&response_type=code&scope=snsapi_base#wechat_redirect");
            response.sendRedirect(sb.toString());
        } catch (UnsupportedEncodingException e) {
            log.error("重定向url编码失败:>>" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            log.error("response重定向失败:>>" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * @Title: login
     * @Description: TODO(首页)
     * @param @return    设定文件
     * @return String    返回类型
     * @throws
     */
    @RequestMapping("/")
    public String login(Model model, HttpServletRequest request, HttpServletResponse response) throws BusinessException {

        //获取重定向携带的code参数值
        String code = request.getParameter("code");

        String currentPageUrl = request.getRequestURL().toString();

        if(StringUtils.isNotBlank(code)) {

            currentPageUrl = new StringBuffer("https://").append(domain).append("/").append("?code=").append(code).append("&state=").toString();

            String openId = null;

            if (null == openId) {
                /*
                 * 根据得到的code参数,内部请求获取openId的方法。
                 */
                openId = getOpenId(request,code);
            }

            /*
             * 将openId保存到session中,当其他业务获取openId时,
             * 可先从session中获取openId.
             */
            request.getSession().setAttribute("openid", openId);
        }
        try {
            String jsapi_ticket = accessTokerService.getWeiXinTicket();
            Map<String, String> ret = WxJsapiSignUtil.sign(jsapi_ticket, currentPageUrl);
            Map<String, String> wxconfig = new HashMap<>();
            wxconfig.put("appId", myWXPayConfig.getAppID());
            wxconfig.put("timestamp", ret.get("timestamp"));
            wxconfig.put("nonceStr", ret.get("nonceStr"));
            wxconfig.put("signature", ret.get("signature"));
            model.addAttribute("wxconfig", wxconfig);

            log.info("wxconfig ="+wxconfig.toString());

        } catch (Exception e) {
            log.error(e.toString());
        }

        return "course/index";
    }

    //发送请求,根据code获取openId
    private String getOpenId(HttpServletRequest request,String code) {

        String content = "";
        String openId = "";
        //封装获取openId的微信API
        StringBuffer url = new StringBuffer();
        url.append("https://api.weixin.qq.com/sns/oauth2/access_token?appid=")
                .append(myWXPayConfig.getAppID())
                .append("&secret=")
                .append(myWXPayConfig.getAppsecret())
                .append("&code=")
                .append(code)
                .append("&grant_type=authorization_code");

        try {
            content = restTemplate.getForEntity(url.toString(), String.class).getBody();

            JSONObject json = JSONObject.parseObject(content);
            openId = json.getString("openid");

        } catch (Exception e) {
            log.error("http获取openId请求失败:", e);
        }
        return openId;
    }
}
2.发送模板消息

以发送公众号模板消息为例,展示openid的使用。

第一步 获取accessToken

@Service
@Slf4j
public class WxAccessTokenServiceImpl implements IWxAccessTokerService {

    private static AccessToken accessToken = null;

    @Resource
    private MyWXPayConfig wxPayConfig;

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 获取access_token的接口地址
     */
    public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

    /**
     * 通过APPID 和 APPSECRET
     * 获取assess_token
     * @return
     */
    @Override
    public AccessToken getAccessToken() throws BusinessException {

        String appid = wxPayConfig.getAppID();
        String appsecret = wxPayConfig.getAppsecret();

        if(accessToken == null
                || DateUtils.addSeconds(accessToken.getReceiveTime(), accessToken.getExpires_in()).before(new Date())) {
            String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
            JSONObject jsonObject = restTemplate.getForObject(requestUrl,JSONObject.class);
            // 如果请求成功
            if (null != jsonObject && jsonObject.get("access_token")!=null) {
                try {
                    accessToken = new AccessToken();
                    accessToken.setAccess_token(jsonObject.getString("access_token"));
                    accessToken.setExpires_in(jsonObject.getInteger("expires_in"));
                    accessToken.setReceiveTime(new Date());
                    log.info("获取token成功!");
                } catch (JSONException e) {
                    accessToken = null;
                    // 获取token失败
                    log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInteger("errcode"), jsonObject.getString("errmsg"));
                }
            } else {
                throw new BusinessException(ResponseConstants.FAIL.getRetCode(), "获取access_token失败!");
            }
        } else {
            log.info("token在有效期内,直接返回!");
        }

        return accessToken;
    }
}

@Data
public class AccessToken {

    /**
     * 获取到的凭证
     */
    private String access_token;

    /*
     * 凭证得到时间
     */
    private Date receiveTime;

    /**
     * 凭证有效时间,单位:秒
     */
    private int expires_in;

}

第二步 构建模板参数实体类

/**
 * 微信模板类
 */
@Data
@ToString
public class WeChatTemplate implements Serializable {

    private static final long serialVersionUID = 612571563869874653L;
    /**
     * 模板id
     */
    private String template_id;

    /**
     * 接收者 openId
     */
    private String touser;

    /**
     * 模板跳转链接
     */
    @JsonSerialize(include= JsonSerialize.Inclusion.NON_EMPTY)
    private String url;

    /**
     * data的数据
     */
    private TreeMap<String, TreeMap<String, String>> data;

    /**
     * data 里的数据
     *
     * @param value :模板参数
     * @param color :颜色 可选
     * @return
     */
    public static TreeMap<String, String> item(String value, String color) {
        TreeMap<String, String> params = new TreeMap<String, String>();
        params.put("value", value);
        params.put("color", color==null?"#173177":color);
        return params;
    }
}

第三步 模板消息发送服务

@Component
@Slf4j
public class WeChatTemplateMessageService {

    @Autowired
    private RestTemplate restTemplate;

    @Resource
    private WechatTemplateMessageLogMapper templateMessageLogMapper;

    /**发送模板消息*/
    public static final String SEND_TEMPLATE_MESSAGE = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";

    /**
     * 发送模板消息
     * @param accessToken
     * @param template
     * @return
     */
    public void sendTemplateMsg(String accessToken, WeChatTemplate template){

        log.info("模板消息请求参数"+ template.toString());

        String requestUrl =SEND_TEMPLATE_MESSAGE.replace("ACCESS_TOKEN",accessToken);
        JSONObject jsonObject = restTemplate.postForObject(requestUrl,template,JSONObject.class);
        log.info("返回jsonObject值:"+jsonObject);
        if (null != jsonObject) {
            int errorCode = jsonObject.getIntValue("errcode");

            //发送日志
       
            if (0 == errorCode) {
                log.info("模板消息发送成功");
            } else {
                String errorMsg = jsonObject.getString("errmsg");
                log.info("模板消息发送失败,错误是 "+errorCode+",错误信息是"+ errorMsg);
            }
        }
    }

}

最后 发送消息代码

try {
            //发送消息
            AccessToken accessToken = wxAccessTokerService.getAccessToken();
            if(accessToken == null) {
                logger.error("accessToken为空,无法发送消息");
            } else {

                WechatTemplateMessageConfig config = templateMessageConfigMapper.selectByCode("APPLY");
                if(StringUtils.isBlank(config.getTemplate_id())) {
                    logger.error("模板id未配置,无法发送消息");
                } else {
                    String first = "您已报名参加!";
                    allData.put("first", WeChatTemplate.item(first, null));
                    allData.put("keyword1",WeChatTemplate.item(TextFormatUtil.towDecimalPlacesFormat(courseApply.getTotal_fee())+"元",null));
                    allData.put("keyword2",WeChatTemplate.item(courseUser.getName(),null));
                    allData.put("keyword3",WeChatTemplate.item(courseUser.getMobile(),null));
                    allData.put("keyword4",WeChatTemplate.item(TextFormatUtil.timeFormatText(courseApply.getApply_time()),null));
                    allData.put("keyword5",WeChatTemplate.item("微信公众号", null));
                    allData.put("remark",WeChatTemplate.item("感谢您的参与!", null));
                    WeChatTemplate template = new WeChatTemplate();
                    template.setTouser(courseUser.getOpenid());
                    template.setTemplate_id(config.getTemplate_id());
                    template.setUrl(null);
                    template.setData(allData);
                    weChatTemplateMessageService.sendTemplateMsg(accessToken.getAccess_token(), template);
                }
            }

 }catch (Exception e){
   e.printStackTrace();
   logger.error("发送消息出现异常"+e.getMessage());
 }

至此 获取openid及发送模板消息完成,后续如有时间会完成微信jssdk及微信支付内容!如有发现错误及不足还请帮忙指正,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容