<file_sep>package com.foamvalue.wechat.service;

import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import com.foamvalue.wechat.dao.DaoImpl;
import com.foamvalue.wechat.utils.WechatUtils;

import io.vertx.core.json.JsonArray;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

/**
 * @ClassName: WechatService
 * @Description: 业务逻辑类
 * @author FoamValue <EMAIL>
 * @date 2016年1月25日 下午3:06:10
 *  
 */
public class WechatService {
  
  private static final Logger LOG_MSG = LoggerFactory.getLogger("ChatUtils");  // 日志文件
  
  public static final String CHAT_FORMAT = "<xml><ToUserName><![CDATA[toUser]]></ToUserName><Encrypt><![CDATA[%1$s]]></Encrypt></xml>";
  
  // 多客服 XML 模版
  public static final String TRANSFER_CUSTOMER = "<xml><ToUserName><![CDATA[%1$s]]></ToUserName><FromUserName><![CDATA[%2$s]]></FromUserName><CreateTime>%3$s</CreateTime><MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>";
  
  // 文本 XML 模版
  public static final String TEXT_FORMAT = "<xml><ToUserName><![CDATA[%1$s]]></ToUserName><FromUserName><![CDATA[%2$s]]></FromUserName><CreateTime>%3$s</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%4$s]]></Content></xml>";
  
  /**
   * @param msg
   * @return
   */
  public static final String fvLogChatIns(Map<String, String> msg) {
    StringBuilder sql = new StringBuilder();
    sql.append("INSERT INTO `fv-vertx`.`fv_log_chat`(`msg_type`,`to_user_name`,`from_user_name`,`create_time`");
    switch (msg.get("MsgType")) {
      case "text":
        sql.append(",`msg_id`,`content`) ");
        break;
      case "image":
        sql.append(",`msg_id`,`pic_url`,`media_id`) ");
        break;
      case "voice":
        if (msg.get("Recognition") != null) {
          sql.append(",`msg_id`,`media_id`,`format`, `recognition`) ");
        } else {
          sql.append(",`msg_id`,`media_id`,`format`) ");
        }
        break;
      case "video":
      case "shortvideo":
        sql.append(",`msg_id`,`media_id`,`thumb_media_id`) ");
        break;
      case "location":
        sql.append(",`msg_id`,`location_x`,`location_y`,`scale`,`label`) ");
        break;
      case "link":
        sql.append(",`msg_id`,`title`,`description`,`url`) ");
        break;
      case "event": // TODO 事件消息
        switch(msg.get("Event")) {
        case "unsubscribe":
        case "subscribe":
          if (StringUtils.isBlank(msg.get("EventKey"))) {
            sql.append(",`event`) ");
            break;
          }
        case "SCAN": 
          sql.append(",`event`, `event_key`, `ticket`) ");
          break;
        case "LOCATION":
          sql.append(",`event`, `latitude`, `longitude`, `precision`) ");
          break;
        case "CLICK":
        case "VIEW":
          sql.append(",`event`, `event_key`) ");
          break;
        }
        break;
    };

    sql.append("VALUES ('").append(msg.get("MsgType")).append("','").append(msg.get("ToUserName")).append("','").append(msg.get("FromUserName")).append("','").append(msg.get("CreateTime")).append("'");
    switch (msg.get("MsgType")) {
      case "text":
        sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("Content")).append("')");
        break;
      case "image":
        sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("PicUrl")).append("','").append(msg.get("MediaId")).append("')");
        break;
      case "voice":
        if (msg.get("Recognition") != null) {
          sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("MediaId")).append("','").append(msg.get("Format")).append("','").append(WechatUtils.transcoding(msg.get("Recognition"))).append("')");
        } else {
          sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("MediaId")).append("','").append(msg.get("Format")).append("')");
        }
        break;
      case "video":
      case "shortvideo":
        sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("MediaId")).append("','").append(msg.get("ThumbMediaId")).append("')");
        break;
      case "location":
        sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("Location_X")).append("','").append(msg.get("Location_Y")).append("','").append(msg.get("Scale")).append("','").append(msg.get("Label")).append("')");
        break;
      case "link":
        sql.append(",'").append(msg.get("MsgId")).append("','").append(msg.get("Title")).append("','").append(msg.get("Description")).append("','").append(msg.get("Url")).append("')");
        break;
      case "event": // TODO 事件消息
        switch(msg.get("Event")) {
        case "unsubscribe":
        case "subscribe":
          if (StringUtils.isBlank(msg.get("EventKey"))) {
            sql.append(",'").append(msg.get("Event")).append("')");
            break;
          }
        case "SCAN": 
          sql.append(",'").append(msg.get("Event")).append("','").append(msg.get("EventKey")).append("','").append(msg.get("Ticket")).append("')");
          break;
        case "LOCATION":
          sql.append(",'").append(msg.get("Event")).append("','").append(msg.get("Latitude")).append("','").append(msg.get("Longitude")).append("','").append(msg.get("Precision")).append("')");
          break;
        case "CLICK":
        case "VIEW":
          sql.append(",'").append(msg.get("Event")).append("','").append(msg.get("EventKey")).append("')");
          break;
        }
        break;
    };
    LOG_MSG.debug(sql.toString());
    return sql.toString();
  }

  /**
   * 取消订阅
   * @param connection
   * @param openId
   */
  public static final void unsubscribe(String openId) {
    DaoImpl.update("update `fv-vertx`.`fv_chat_fans` set `subscribe` = 0, `fv_upd_time` = now() where `open_id` = ?", updRes -> {
      if (updRes.failed()) {
        LOG_MSG.error(updRes.cause());
        return;
      }
    });
  }
  
  /**
   * 订阅
   * @param connection
   * @param openId
   * @param eventKey 事件KEY值，qrscene_为前缀，后面为二维码的参数值
   * @param ticket 二维码的ticket，可用来换取二维码图片
   */
  public static final void subscribe(String openId, String eventKey, String ticket) {
    DaoImpl.queryWithParams("select count(1) from fv_chat_fans where open_id = ?", new JsonArray().add(openId), selRes -> {
      if (selRes.failed()) {
        LOG_MSG.error(selRes.cause());
        return;
      }
      Long count = selRes.result().getResults().get(0).getLong(0); // 获取行数
      Boolean isScan = StringUtils.isNotBlank(eventKey) && StringUtils.isNotBlank(ticket);
      if (count == 0L) {
        StringBuilder insSql = new StringBuilder();
        insSql.append("INSERT INTO `fv-vertx`.`fv_chat_fans`(`open_id`,`subscribe`");
        if (isScan) {
          insSql.append(",`event_key`,`ticket`");
        }
        insSql.append(") VALUES('")
          .append(openId)
          .append("', 1");
        if (isScan) {
          insSql.append(", '")
            .append(eventKey)
            .append("', '")
            .append(ticket)
            .append("'");
        }
        insSql.append(")");
        
        DaoImpl.update(insSql.toString(), insRes -> {
          if (insRes.failed()) {
            LOG_MSG.error(insRes.cause());
            return;
          }
        });
      } else {
        StringBuilder updSql = new StringBuilder();
        updSql.append("update `fv-vertx`.`fv_chat_fans` set `subscribe` = 0, `fv_upd_time` = now()");
        if (isScan) {
          updSql.append(", `event_key` = '")
            .append(eventKey)
            .append("', `ticket` = '")
            .append(ticket)
            .append("' ");
        }
        updSql.append("where `open_id` = '")
          .append(openId)
          .append("'");
        DaoImpl.update(updSql.toString(), updRes -> {
          if (updRes.failed()) {
            LOG_MSG.error(updRes.cause());
            return;
          }
        });
      }
    });
  }
  
  /**
   * 扫码
   * @param connection
   * @param openId
   * @param eventKey
   * @param ticket
   */
  public static final void scan(String openId, String eventKey, String ticket) {
    DaoImpl.updateWithParams("INSERT INTO `fv-vertx`.`fv_chat_fans_scan`(`open_id`,`event_key`,`ticket`)VALUES(?,?,?)", 
        new JsonArray().add(openId).add(eventKey).add(ticket), insRes -> {
      if (insRes.failed()) {
        LOG_MSG.error(insRes.cause());
        return;
      }
    });
  }

  /**
   * 上报地理位置信息
   * @param client
   * @param openId
   * @param latitude
   * @param longitude
   * @param precision
   */
  public static final void location(String openId, String latitude, String longitude, String precision) {
    DaoImpl.updateWithParams("INSERT INTO `fv-vertx`.`fv_chat_fans_location`(`open_id`,`latitude`,`longitude`,`precision`)VALUES(?, ?, ?, ?)",
        new JsonArray().add(openId).add(latitude).add(longitude).add(precision), insRes -> {
      if (insRes.failed()) {
        LOG_MSG.error(insRes.cause());
        return;
      }
    });
  }
}
<file_sep>package com.foamvalue.wechat.dao;

import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.ResultSet;
import io.vertx.ext.sql.SQLConnection;
import io.vertx.ext.sql.UpdateResult;

/**
 * @ClassName: DaoImpl
 * @Description: TODO 数据库操作类
 * @author FoamValue <EMAIL>
 * @date 2016年1月26日 上午10:36:15
 * 
 */
public class DaoImpl {

  private static final Logger LOG_MSG = LoggerFactory.getLogger("DaoImpl"); // 日志文件
  private static JDBCClient client; // 异步 JDBC 客户端

  /**
   * @param vertx
   * @param config
   */
  public DaoImpl(Vertx vertx, JsonObject config) {
    client = JDBCClient.createShared(vertx, config);
  }

  /**
   * @param sql
   * @param resultHandler
   */
  public static void execute(String sql, Handler<AsyncResult<Void>> resultHandler) {
    client.getConnection(conn -> {
      if (conn.failed()) {
        LOG_MSG.error(conn.cause().getMessage());
        return;
      }
      final SQLConnection connection = conn.result();
      try {
        connection.execute(sql, resultHandler);
        closeConn(connection);
      } catch (Exception e) {
        closeConn(connection);
        LOG_MSG.error(e.toString());
      }
    });
  }

  /**
   * @param sql
   * @param resultHandler
   */
  public static void query(String sql, Handler<AsyncResult<ResultSet>> resultHandler) {
    client.getConnection(conn -> {
      if (conn.failed()) {
        LOG_MSG.error(conn.cause().getMessage());
        return;
      }
      final SQLConnection connection = conn.result();
      try {
        connection.query(sql, resultHandler);
        closeConn(connection);
      } catch (Exception e) {
        closeConn(connection);
        LOG_MSG.error(e.toString());
      }
    });
  }

  /**
   * @param sql
   * @param params
   * @param resultHandler
   */
  public static void updateWithParams(String sql, JsonArray params, Handler<AsyncResult<UpdateResult>> resultHandler) {
    client.getConnection(conn -> {
      if (conn.failed()) {
        LOG_MSG.error(conn.cause().getMessage());
        return;
      }
      final SQLConnection connection = conn.result();
      try {
        connection.updateWithParams(sql, params, resultHandler);
        closeConn(connection);
      } catch (Exception e) {
        closeConn(connection);
        LOG_MSG.error(e.toString());
      }
    });
  }

  /**
   * @param sql
   * @param params
   * @param resultHandler
   */
  public static void queryWithParams(String sql, JsonArray params, Handler<AsyncResult<ResultSet>> resultHandler) {
    client.getConnection(conn -> {
      if (conn.failed()) {
        LOG_MSG.error(conn.cause().getMessage());
        return;
      }
      final SQLConnection connection = conn.result();
      try {
        connection.queryWithParams(sql, params, resultHandler);
        closeConn(connection);
      } catch (Exception e) {
        closeConn(connection);
        LOG_MSG.error(e.toString());
      }
    });
  }

  /**
   * @param sql
   * @param resultHandler
   */
  public static void update(String sql, Handler<AsyncResult<UpdateResult>> resultHandler) {
    client.getConnection(conn -> {
      if (conn.failed()) {
        LOG_MSG.error(conn.cause().getMessage());
        return;
      }
      final SQLConnection connection = conn.result();
      try {
        connection.update(sql, resultHandler);
        closeConn(connection);
      } catch (Exception e) {
        closeConn(connection);
        LOG_MSG.error(e.toString());
      }
    });
  }

  /**
   * 关闭数据库链接
   * 
   * @param connection
   */
  private static void closeConn(SQLConnection connection) {
    connection.close(done -> {
      if (done.failed()) {
        LOG_MSG.error(done.cause().getMessage());
      }
    });
  }
}
<file_sep>package com.foamvalue.wechat;

import com.foamvalue.wechat.controller.Wechat;
import com.foamvalue.wechat.dao.DaoImpl;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.json.JsonObject;

/**
 * @ClassName: FoamValue
 * @Description: Vert.x Web Verticle 实现类
 * @author FoamValue <EMAIL>
 * @date 2016年1月14日 下午4:12:07
 * 
 */
public class FoamValue extends AbstractVerticle {
  
  //地址命名约定 CN.LIVE + ":" + SERVICE NAME + ":" + EVENT NAME + ":" + GET/POST
  private static final String CHAT_GET_ADDRESS = "CN.LIVE:WECHAT:CHAT:GET";
  private static final String CHAT_POST_ADDRESS = "CN.LIVE:WECHAT:CHAT:POST";
  
  /**
   * TODO 部署时，替换成 config(); 
   * java -jar target/fv-rertx-0.0.1-SNAPSHOT-fat.jar -conf src/main/resources/application.json
   */
  private static final JsonObject CONFIG = new JsonObject()
      .put("url", "jdbc:mysql://localhost:3306/fv-vertx?useUnicode=true&amp;characterEncoding=UTF-8")
      .put("driver_class", "com.mysql.jdbc.Driver")
      .put("user", "root")
      .put("password", "<PASSWORD>")
      .put("max_pool_size", 10);

  @Override
  public void start() {
    EventBus eb = vertx.eventBus();
    try {
      new DaoImpl(vertx, CONFIG);
      eb.consumer(CHAT_GET_ADDRESS, message -> {Wechat.doGet(message);});
      eb.consumer(CHAT_POST_ADDRESS, message -> {Wechat.doPost(message);});
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
<file_sep><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.foamvalue</groupId>
	<artifactId>x-parent</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>x-parent</name>
	<packaging>pom</packaging>

	<modules>
		<module>x-common</module>
	</modules>

	<properties>
		<!-- 项目版本 -->
		<project.encoding>UTF-8</project.encoding>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

		<project.version>0.0.1-SNAPSHOT</project.version>

		<vertx.version>3.2.0</vertx.version>
		<junit.version>4.12</junit.version>
		<slf4j.version>1.7.14</slf4j.version>
		<log4j.version>1.2.17</log4j.version>
		<gson.version>2.5</gson.version>
		<commons.lang3.version>3.4</commons.lang3.version>
		<commons.codec.version>1.10</commons.codec.version>
		<dom4j.version>1.6.1</dom4j.version>
		<mysql.version>5.1.37</mysql.version>
	</properties>

	<dependencyManagement>
		<dependencies>
			<!-- vertx -->
			<dependency>
				<groupId>io.vertx</groupId>
				<artifactId>vertx-core</artifactId>
				<version>${vertx.version}</version>
			</dependency>
			<dependency>
				<groupId>io.vertx</groupId>
				<artifactId>vertx-web</artifactId>
				<version>${vertx.version}</version>
			</dependency>
			<dependency>
				<groupId>io.vertx</groupId>
				<artifactId>vertx-hazelcast</artifactId>
				<version>${vertx.version}</version>
			</dependency>
			<dependency>
				<groupId>io.vertx</groupId>
				<artifactId>vertx-jdbc-client</artifactId>
				<version>${vertx.version}</version>
			</dependency>

			<!-- log -->
			<dependency>
				<groupId>org.slf4j</groupId>
				<artifactId>slf4j-api</artifactId>
				<version>${slf4j.version}</version>
			</dependency>
			<dependency>
				<groupId>org.slf4j</groupId>
				<artifactId>slf4j-log4j12</artifactId>
				<version>${slf4j.version}</version>
			</dependency>
			<dependency>
				<groupId>log4j</groupId>
				<artifactId>log4j</artifactId>
				<version>${log4j.version}</version>
			</dependency>

			<!-- tool -->

			<dependency>
				<groupId>com.google.code.gson</groupId>
				<artifactId>gson</artifactId>
				<version>${gson.version}</version>
			</dependency>

			<dependency>
				<groupId>org.apache.commons</groupId>
				<artifactId>commons-lang3</artifactId>
				<version>${commons.lang3.version}</version>
			</dependency>

			<dependency>
				<groupId>commons-codec</groupId>
				<artifactId>commons-codec</artifactId>
				<version>${commons.codec.version}</version>
			</dependency>

			<dependency>
				<groupId>dom4j</groupId>
				<artifactId>dom4j</artifactId>
				<version>${dom4j.version}</version>
			</dependency>

			<!-- mysql -->
			<dependency>
				<groupId>mysql</groupId>
				<artifactId>mysql-connector-java</artifactId>
				<version>${mysql.version}</version>
			</dependency>

			<!-- test -->
			<dependency>
				<groupId>junit</groupId>
				<artifactId>junit</artifactId>
				<version>${junit.version}</version>
				<scope>test</scope>
			</dependency>
			<dependency>
				<groupId>io.vertx</groupId>
				<artifactId>vertx-unit</artifactId>
				<version>${vertx.version}</version>
				<scope>test</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

</project><file_sep># Vert.x
Micro Services
<file_sep>package com.foamvalue.common.utils;

import io.vertx.core.http.HttpServerResponse;

/**
 * @ClassName: ResultMsgUtils
 * @Description: TODO 返回信息工具
 * @author FoamValue <EMAIL>
 * @date 2016年3月7日 下午8:30:26
 *  
 */
public class ResultMsgUtils {
  
  /**
   * 返回错误码
   * 
   * @param routingContext
   * @param code
   */
  public static void returnMSG(HttpServerResponse response) {
    returnMSG(response, "");
  }
  
  public static void returnMSG(HttpServerResponse response, ResultCodeEnum code) {
    returnMSG(response, "错误码：" + code.getResultCode());
  }
  
  public static void returnMSG(HttpServerResponse response, String msg) {
    response.setChunked(true);
    response.write(msg); // Write a String to the response body, encoded in UTF-8.
    response.end(); // Once the response has ended, it cannot be used any more.
  }
  
  public static void returnMSG(HttpServerResponse response, int statusCode) {
    response.setStatusCode(statusCode).end();
  }
}<file_sep>package com.foamvalue.eventBus.router;

import com.foamvalue.common.utils.ResultMsgUtils;

import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

/**
 * @ClassName: WebsiteRouter
 * @Description: TODO fv-website
 * @author FoamValue <EMAIL>
 * @date 2016年2月25日 下午2:08:15
 *  
 */
public class WebsiteRouter {
  
  // WebSite
  // 地址命名约定 CN.LIVE + ":" + SERVICE NAME + ":" + EVENT NAME + ":" + GET/POST
  private static final String WEBSITE_ADDRESS = "CN.LIVE:WEBSITE";
  
  /**
   * 设置 fv-website 路由 
   * @param router
   * @param vertx
   */
  public static void setRouter(Router router, Vertx vertx) {
    router.route().handler(new Handler<RoutingContext>() {
      @Override
      public void handle(RoutingContext event) {
        vertx.eventBus().send(WEBSITE_ADDRESS, null, reply -> {
          if (reply.succeeded()) {
            Object rep = reply.result().body();
            if (rep != null) {
              ResultMsgUtils.returnMSG(event.response(), rep.toString());
            } else {
              ResultMsgUtils.returnMSG(event.response(), "");
            }
          } else {
            ResultMsgUtils.returnMSG(event.response(), "");
          }
        });
      }
    });
  }

}
<file_sep>package com.foamvalue.eventBus;

import com.foamvalue.eventBus.router.WeChatRouter;
import com.foamvalue.eventBus.router.WebsiteRouter;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;

/**
 * @ClassName: FoamValue
 * @Description: 主服务
 * @author FoamValue <EMAIL>
 * @date 2016年2月18日 下午5:03:44
 * 
 */
public class FoamValue extends AbstractVerticle {

  private static Router router;
  private static final Logger LOG_MSG = LoggerFactory.getLogger("VertxHttpServer"); // 系统日志

  @Override
  public void start(Future<Void> fut) throws Exception {
    router = Router.router(vertx); // 接受 HTTP 请求并转发给**第一个**匹配的路径

    /**
     * 服务化
     * **优势**
     * 服务松耦合原则：
     * 1. 时间松耦合，异步操作保持高吞吐量；
     * 2. 位置松耦合，地址访问不需要知道实际位置；
     * 3. 版本松耦合，不依赖固定版本提供服务；
     * 
     * **不足**
     * 1. 提供新服务时，必须更新主服务。
     * 2. 版本松耦合，功能必须向下兼容。
     * 
     */
    router.route().handler(BodyHandler.create());
    WeChatRouter.setRouter(router, vertx); // fv-wechat 路由
    WebsiteRouter.setRouter(router, vertx); // TODO fv-website
    
    LOG_MSG.info("Creating HTTP server for vert.x web application");
    vertx.createHttpServer(new HttpServerOptions().setMaxWebsocketFrameSize(1000000)).requestHandler(router::accept).listen(80, result -> {
      if (result.succeeded()) {
        fut.complete();
      } else {
        fut.fail(result.cause());
      }
    });
  }
}
