用友 OpenAPI

发布时间 2024-01-05 12:07:51作者: 大道至简0

1. OpenAPI含义

1.1 什么是OpenAPI

Open API即开放API,也称开放平台。 所谓的开放API(OpenAPI)是服务型网站常见的一种应用,网站的服务商将自己的网站服务封装成一系列API(Application Programming Interface,应用编程接口)开放出去,供第三方开发者使用,这种行为就叫做开放网站的API,所开放的API就被称作OpenAPI(开放API)。
网站提供开放平台的API后,可以吸引一些第三方的开发人员在该平台上开发商业应用,平台提供商可以获得更多的流量与市场份额,第三方开发者不需要庞大的硬件与技术投资就可以轻松快捷的创业,从而达到双赢的目的,开放API是大平台发展、共享的途径,让开发者开发一个有价值应用,付出的成本更少,成功的机会更多。今天,OpenAPI作为互联网在线服务的发展基础,已经成为越来越多互联网企业发展服务的必然选择。

1.2 Uap实现openAPI

就现在互联网上Open API的形态来看,主要分成两种:标准REST和类REST(也可以叫做RPC形态)。
REST形态主要有这么几点特点:
1.服务地址就是资源定位地址。
2.服务操作就是Http请求中的方法类型(GET,POST,DELETE,PUT),这其实是抽象现实当中对于服务的增删改查操作。

Restlet项目为“建立REST概念与Java类之间的映射”提供了一个轻量级而全面的框架。

UAP在Restlet框架之上,选择了官方JAX-RS扩展,并且在扩展的基础上与NC进行了集成。

2. OpenAPI的开发

2.1 开发前准备工作

请使用平台提供的nchome,并且准备如下两个步骤:

  1. nchome中modules/uapfw路径下包含pubuapfw_restframeworkLevel-1.jar
  2. nchome/hotwebs/nccloud/WEB-INF/web.xml中增加配置,配置如下所示:
	<context-param>
		<param-name>org.restlet.application</param-name>
		<param-value>uap.ws.rest.core.UAPRestJaxRsApplication</param-value>
	</context-param>
 
	<filter>
    	<filter-name>openCloudSecurityFilter</filter-name>
    	<filter-class>nccloud.ws.opm.core.filter.OpenCloudSecurityFilter</filter-class>
  	</filter>
	<!-- opm login-->
	<filter> 
    	 <filter-name>openCloudLoginFilter</filter-name> 
    	 <filter-class>nccloud.ws.opm.core.filter.OpenCloudLoginFilter</filter-class> 
  	</filter>
	<filter-mapping>
		<filter-name>openCloudSecurityFilter</filter-name>
		<url-pattern>/api/*</url-pattern>
	</filter-mapping>
	<filter-mapping>
		<filter-name>openCloudLoginFilter</filter-name>
		<url-pattern>/opm/*</url-pattern>
	</filter-mapping>
	
	<servlet>
		<servlet-name>RestletServlet</servlet-name>
		<servlet-class>uap.ws.rest.servlet.UAPRSServerServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>RestletServlet</servlet-name>
		<url-pattern>/api/*</url-pattern>
	</servlet-mapping>

2.2 OpenAPI开发步骤

2.2.1 创建资源

创建资源类xxResource继承AbstractNCCRestResource

package nccloud.openapi.uapbd.currtype.currtype;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.Path;
 
import org.json.JSONString;
 
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
 
import nc.bs.logging.Logger;
import nc.vo.bd.currtype.CurrtypeVO;
import nccloud.api.rest.utils.NCCRestUtils;
import nccloud.api.rest.utils.ResultMessageUtil;
import nccloud.impl.platform.common.util.QueryUtil;
import nccloud.ws.rest.resource.AbstractNCCRestResource;
 
@Path("uapbd/currtype/currtype")
public class CurrtypeResource extends AbstractNCCRestResource{
 
	@POST
	@Path("queryCurrtypeByCode")
	@Consumes("application/json")
	@Produces("application/json")
	public JSONString queryCurrtypeByCode(JSONString json) {
		
		if (json == null) {
			Logger.error("输入数据异常");
			return ResultMessageUtil.exceptionToJSON(new IllegalArgumentException("输入数据异常"));
		}
		JSONArray jsonarray = JSONObject.parseArray(json.toJSONString());
		if (jsonarray == null || jsonarray.size() == 0) {
			return ResultMessageUtil.exceptionToJSON(new IllegalArgumentException("输入数据异常"));
		}
		// 异常信息
		List<String> msglist = new ArrayList<String>();
		// 查询出的所有数据
		List<CurrtypeVO> allData = new ArrayList<CurrtypeVO>();
		
		String currTypeCode = "";
		for (int i = 0; i < jsonarray.size(); i++) {
			JSONObject jObject = jsonarray.getJSONObject(i);
			currTypeCode = jObject.getString("code");
		}
		
		String sql = " code='" + currTypeCode + "'";
		CurrtypeVO[] vos = QueryUtil.queryVOByCond(CurrtypeVO.class, sql, null);
		if(vos == null || vos.length == 0) {
			return NCCRestUtils.toJSONString("123123");
		}
		else {
			return NCCRestUtils.toJSONString(vos);
		}
	}
	
	@Override
	public String getModule() {
		return "uapbd";
	}
 
}
 

2.2.2 注册资源(rest文件)

<?xml version="1.0" encoding='gb2312'?>
<module>
	<rest>
		<resource classname="nccloud.openapi.uapbd.currtype.currtype.CurrtypeResource"  exinfo=""/>
	</rest>
</module>

2.2.3 添加接口说明(MD文件)

openAPI开发完成以后,需要对外提供接口的使用方法,包括接口能力、调用方式、需要的参数、返回的数据(数据说明,json)。参照的格式如下所示:

# 根据会计期间主键删除会计期间
 
## 1.接口说明
根据会计期间主键删除会计期间
不支持批量删除
## 2.使用场景
根据会计期间主键删除会计期间
## 3.接口调用说明
 
### 3.1请求类型 
post
 
### 3.2请求示例							
> http://IP:port/nccloud/api/uapbd/accperiodmanage/accperiod/deleteAccPeriodByPk
 
### 3.3请求URL参数说明
|参数|类型|是否必填|描述|
|------|------|------|------|
|pk|string[]|是|会计期间主键|
 
> JSON示例
```
{
	"pk": ["123"]
}
```
 
### 3.4返回参数说明
#### 3.4.1正确返回示例
>JSON示例: 
```
[
    {
          "true"
    }
]
```

2.2.4 OpenAPI注册以及接口测试

2.2.4.1 OpenAPI的注册

完成Java类以及rest文件编写之后,还需要进行一步网上资源的注册。步骤如下:
访问地址[http://ip:port/nccloud/resources/opm,并通过管理员登录。](javascript:void(0))
在API维护页签当中新增加相关的API,其中需要注意一下几个点:
URI路径如下所示:/nccloud/api/uapbd/currtype/currtype/queryCurrtypeByCode
其中uapbd/currtype/currtype对应于资源类的Path,而queryCurrtypeByCode对应于相关方法的Path注解。
在第三方应用管理当中进行应用的注册并且分配相关的小应用,然后就可以使用注册app_id,app_secret,加密类型以及公钥字段进行API的访问了。

注册API的步骤如下图所示:

img

注册第三方应用如下图所示:

img

2.2.4.1 OpenAPI的接口测试

API的测试或者请求流程如下所示:
第一步请求获取相应的token,请求的URL路径如下:[http://127.0.0.1/nccloud/opm/accesstoken](javascript:void(0))?
biz_center=1&grant_type=client_credentials&signature=3ca73b3bb506a34059e2bce1ce3bfe128e4e9f
b6cefb7325396094f7816a01d9&client_secret=Su1s4kk0pQhhgAupsDajkqSFWeWxUEAy78yYh84wTH
t1UPyC2ZV3CD7%2BP12XB897owyaVFQRJd2g%0D%0AfZcPwkvlxUgq3yrp1PBYxZ1TJ89oLf4Wicn
%2BsDVAi57pTlsHHZZqQqLow5zdQjNP3Wm04ewszLhu%0D%0AasoViTdspzujiPAmwxY%3D%0D%
0A&client_id=wqch
其中各个参数的含义如下:
biz_center:访问的nccloud系统的账套code
grant_type:授权模式,此处为client_credentials
client_id:对应于在第三方应用注册当中的app_id
client_secret:对应于第三方应用注册当中的app_secret
signature:请求加签,其算法为SHA256Util.getSHA256(client_id + client_secret + pubKey)
其中pubKey为第三方应用注册当中的公钥字段

第二步为最终请求的OpenAPI,其中URL地址如下所示:[http://127.0.0.1/nccloud/api/uapbd/currtype/](javascript:void(0))
currtype/queryCurrtypeByCode
其中在请求头当中包含了相关的参数,如下所示:
access_token:即上一步当中获取到的token。
ucg_flag:为Y。
signature:对请求体进行加签,算法如下:SHA256Util.getSHA256(client_id+requestBody+pubkey)
其中pubKey为第三方应用注册当中的公钥字段。

下面是一段调用接口的Java代码及其配置:

package nccloud.api.testcase.base;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
 
import org.apache.commons.lang3.StringUtils;
 
import com.google.gson.Gson;
 
import nccloud.api.test.utils.CompressUtil;
import nccloud.api.test.utils.Decryption;
import nccloud.api.test.utils.Encryption;
import nccloud.api.test.utils.ResultMessageUtil;
import nccloud.api.test.utils.SHA256Util;
 
/**
 * 1.从resources/config.properties中读取测试api相关的数据 2.运行程序,测试查看测试结果
 * 
 * @author lizhmf
 * @date 2019年6月20日上午10:53:11
 */
public class Test {
 
	private static String client_secret = null;
	private static String pubKey = null;
	private static String client_id = null;
	private static String username = null;
	private static String pwd = null;
	private static String busi_center = null;
	// 获取token方式
	private static String grant_type = null;
	// 服务器ip:port
	private static String baseUrl = null;
	private static String secret_level = null;
	private static String requestBody = null;
	// openapi请求路径
	private static String apiUrl = null;
 
	public static String token = null;
	public static String repeat_check = null;
	public static String busi_id = null;
	
	public static void main(String[] args) {
		try {
			// 初始化数据
			init();
			// 请求token
			token = getToken();
			System.out.println("getTokenData:" + token);
			if (token != null) {
				// 测试openapi
				testApi(token);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
 
	/**
	 * 通过refresh_token重新获取token
	 * 
	 * @param refresh_token
	 * @return
	 * @throws UnsupportedEncodingException
	 * @throws Exception
	 */
	private static String getTokenByRefreshToken(String refresh_token) throws UnsupportedEncodingException, Exception {
		Map<String, String> paramMap = new HashMap<String, String>();
		// 密码模式认证
		paramMap.put("grant_type", "refresh_token");
		// 第三方应用id
		paramMap.put("client_id", client_id);
		// 第三方应用secret 公钥加密
		paramMap.put("client_secret", URLEncoder.encode(Encryption.pubEncrypt(pubKey, client_secret), "utf-8"));
		// 签名
		String sign = SHA256Util.getSHA256(client_id + client_secret + refresh_token + pubKey);
		paramMap.put("signature", sign);
 
		String url = baseUrl + "/nccloud/opm/accesstoken";
		String mediaType = "application/x-www-form-urlencoded";
		String token = doPost(url, paramMap, mediaType, null, "");
		return token;
	}
 
	private static String getToken() throws Exception {
		String token = null;
		if ("password".equals(grant_type)) {
			// 密码模式
			token = getTokenByPWD();
		} else if ("client_credentials".equals(grant_type)) {
			// 客户端模式
			token = getTokenByClient();
		} else if ("authorization_code".equals(grant_type)) {
			// TODO 页面跳转
			// 授权码模式
		}
		return token;
	}
 
	/**
	 * 客户端模式获取token
	 * 
	 * @return
	 * @throws Exception
	 */
	private static String getTokenByClient() throws Exception {
		Map<String, String> paramMap = new HashMap<String, String>();
		// 密码模式认证
		paramMap.put("grant_type", "client_credentials");
		// 第三方应用id
		paramMap.put("client_id", client_id);
		// 第三方应用secret 公钥加密
		paramMap.put("client_secret", URLEncoder.encode(Encryption.pubEncrypt(pubKey, client_secret), "utf-8"));
		// 账套编码
		paramMap.put("biz_center", busi_center);
		// // TODO 传递数据源和ncc登录用户
		// paramMap.put("dsname", "TM_0614");
		// paramMap.put("usercode", "1");
 
		// 签名
		String sign = SHA256Util.getSHA256(client_id + client_secret + pubKey);
		paramMap.put("signature", sign);
 
		String url = baseUrl + "/nccloud/opm/accesstoken";
		String mediaType = "application/x-www-form-urlencoded";
		String token = doPost(url, paramMap, mediaType, null, "");
		return token;
	}
 
	/**
	 * 密码模式获取token
	 * 
	 * @return
	 * @throws Exception
	 */
	@SuppressWarnings("unused")
	private static String getTokenByPWD() throws Exception {
		Map<String, String> paramMap = new HashMap<String, String>();
		// 密码模式认证
		paramMap.put("grant_type", "password");
		// 第三方应用id
		paramMap.put("client_id", client_id);
		// 第三方应用secret 公钥加密
		paramMap.put("client_secret", URLEncoder.encode(Encryption.pubEncrypt(pubKey, client_secret), "utf-8"));
		// ncc用户名
		paramMap.put("username", username);
		// 密码 公钥加密
		paramMap.put("password", URLEncoder.encode(Encryption.pubEncrypt(pubKey, pwd), "utf-8"));
		// 账套编码
		paramMap.put("biz_center", busi_center);
		// 签名
		String sign = SHA256Util.getSHA256(client_id + client_secret + username + pwd + pubKey);
		paramMap.put("signature", sign);
 
		String url = baseUrl + "/nccloud/opm/accesstoken";
		String mediaType = "application/x-www-form-urlencoded";
		String token = doPost(url, paramMap, mediaType, null, "");
		return token;
	}
 
	/**
	 * 请求openapi
	 * 
	 * @param token
	 * @param security_key
	 *                         请求body参数加密压缩用的key
	 * @throws Exception
	 */
	private static void testApi(String token) throws Exception {
		// token转对象,获取api访问所用token和secret
		ResultMessageUtil returnData = new Gson().fromJson(token, ResultMessageUtil.class);
		Map<String, String> data = (Map<String, String>) returnData.getData();
		String access_token = data.get("access_token");
		String security_key = data.get("security_key");
		String refresh_token = data.get("refresh_token");
		System.out.println("【ACCESS_TOKEN】:" + access_token);
 
		// 请求路径
		String url = baseUrl + apiUrl;
		// header 参数
		Map<String,String> headermap = new HashMap<>();
		headermap.put("access_token", access_token);
		headermap.put("client_id", client_id);
		
		StringBuffer sb = new StringBuffer();
		sb.append(client_id);
		if (StringUtils.isNotBlank(requestBody)) {
			// sb.append(requestBody.replaceAll("\\s*|\t|\r|\n", "").trim());
			sb.append(requestBody);
		}
		sb.append(pubKey);
		String sign = SHA256Util.getSHA256(sb.toString());
		headermap.put("signature", sign);
 
		if (StringUtils.isNotBlank(busi_id)) {
			headermap.put("busi_id", busi_id);
		}
		if (StringUtils.isNotBlank(repeat_check)) {
			headermap.put("repeat_check", repeat_check);
		}
		headermap.put("ucg_flag", "y");
 
		String mediaType = "application/json;charset=utf-8";
 
		// 表体数据json
		// 根据安全级别选择加密或压缩请求表体参数
		String json = dealRequestBody(requestBody, security_key, secret_level);
 
		// 返回值
		String result = doPost(url, null, mediaType, headermap, json);
		String result2 = dealResponseBody(result, security_key, secret_level);
		System.out.println("【RESULT】:" + result);
		// System.out.println("result解密:" + result2);
	}
 
	private static String dealResponseBody(String source, String security_key, String level) throws Exception {
		String result = null;
 
		if (StringUtils.isEmpty(level) || SecretConst.LEVEL0.equals(level)) {
			result = source;
		} else if (SecretConst.LEVEL1.equals(level)) {
			result = Decryption.symDecrypt(security_key, source);
		} else if (SecretConst.LEVEL2.equals(level)) {
			result = CompressUtil.gzipDecompress(source);
		} else if (SecretConst.LEVEL3.equals(level)) {
			result = CompressUtil.gzipDecompress(Decryption.symDecrypt(security_key, source));
		} else if (SecretConst.LEVEL4.equals(level)) {
			result = Decryption.symDecrypt(security_key, CompressUtil.gzipDecompress(source));
		} else {
			throw new Exception("无效的安全等级");
		}
 
		return result;
	}
 
	/**
	 * 初始化参数
	 */
	private static void init() {
		// TODO Auto-generated method stub
		Properties properties = new Properties();
 
		String filepath = "config.properties";
		ClassLoader classloader = Thread.currentThread().getContextClassLoader();
		InputStream inputStream = classloader.getResourceAsStream(filepath);
		try {
			InputStreamReader reader = new InputStreamReader(inputStream, "UTF-8");
			properties.load(reader);
 
			client_secret = new String(properties.getProperty("client_secret").getBytes("utf-8"), "utf-8");
			client_id = properties.getProperty("client_id");
			pubKey = properties.getProperty("pubKey");
			username = properties.getProperty("username");
			pwd = properties.getProperty("pwd");
			busi_center = properties.getProperty("busi_center");
			baseUrl = properties.getProperty("baseUrl");
			requestBody = new String(properties.getProperty("requestBody").getBytes("utf-8"), "utf-8");
			apiUrl = properties.getProperty("apiUrl");
			grant_type = properties.getProperty("grant_type");
			secret_level = properties.getProperty("secret_level");
			repeat_check = properties.getProperty("repeat_check");
			busi_id = properties.getProperty("busi_id");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
 
	// 根据安全级别设置,表体是否加密或压缩
	private static String dealRequestBody(String source, String security_key, String level) throws Exception {
		String result = null;
		if (StringUtils.isEmpty(level) || SecretConst.LEVEL0.equals(level)) {
			result = source;
		} else if (SecretConst.LEVEL1.equals(level)) {
			result = Encryption.symEncrypt(security_key, source);
		} else if (SecretConst.LEVEL2.equals(level)) {
			result = CompressUtil.gzipCompress(source);
		} else if (SecretConst.LEVEL3.equals(level)) {
			result = Encryption.symEncrypt(security_key, CompressUtil.gzipCompress(source));
		} else if (SecretConst.LEVEL4.equals(level)) {
			result = CompressUtil.gzipCompress(Encryption.symEncrypt(security_key, source));
		} else {
			throw new Exception("无效的安全等级");
		}
 
		return result;
	}
 
	/**
	 * 发送post请求
	 * 
	 * @param baseUrl
	 * @param paramMap
	 * @param mediaType
	 * @param headers
	 * @param json
	 * @return
	 */
	private static String doPost(String baseUrl, Map<String, String> paramMap, String mediaType, Map<String, String> headers, String json) {
		
		HttpURLConnection urlConnection = null;
		InputStream in = null;
		OutputStream out = null;
		BufferedReader bufferedReader = null;
		String result = null;
		try {
			StringBuffer sb = new StringBuffer();
			sb.append(baseUrl);
			if (paramMap != null) {
				sb.append("?");
				for (Map.Entry<String, String> entry : paramMap.entrySet()) {
					String key = entry.getKey();
					String value = entry.getValue();
					sb.append(key + "=" + value).append("&");
				}
				baseUrl = sb.toString().substring(0, sb.toString().length() - 1);
			}
 
			URL urlObj = new URL(baseUrl);
			urlConnection = (HttpURLConnection) urlObj.openConnection();
			urlConnection.setConnectTimeout(50000);
			urlConnection.setRequestMethod("POST");
			urlConnection.setDoOutput(true);
			urlConnection.setDoInput(true);
			urlConnection.setUseCaches(false);
			urlConnection.addRequestProperty("content-type", mediaType);
			if (headers != null) {
				for (String key : headers.keySet()) {
					urlConnection.addRequestProperty(key, headers.get(key));
				}
			}
			out = urlConnection.getOutputStream();
			out.write(json.getBytes("utf-8"));
			out.flush();
			int resCode = urlConnection.getResponseCode();
			if (resCode == HttpURLConnection.HTTP_OK || resCode == HttpURLConnection.HTTP_CREATED || resCode == HttpURLConnection.HTTP_ACCEPTED) {
				in = urlConnection.getInputStream();
			} else {
				in = urlConnection.getErrorStream();
			}
			bufferedReader = new BufferedReader(new InputStreamReader(in, "utf-8"));
			StringBuffer temp = new StringBuffer();
			String line = bufferedReader.readLine();
			while (line != null) {
				temp.append(line).append("\r\n");
				line = bufferedReader.readLine();
			}
			String ecod = urlConnection.getContentEncoding();
			if (ecod == null) {
				ecod = Charset.forName("utf-8").name();
			}
			result = new String(temp.toString().getBytes("utf-8"), ecod);
		} catch (Exception e) {
			System.out.println(e);
		} finally {
			if (null != bufferedReader) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (null != out) {
				try {
					out.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (null != in) {
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			urlConnection.disconnect();
		}
		return result;
	}
 
	class SecretConst {
		/**
		 * LEVEL0 不压缩、不加密
		 */
		public static final String LEVEL0 = "L0";
		/**
		 * LEVEL1 只加密、不压缩
		 */
		public static final String LEVEL1 = "L1";
		/**
		 * LEVEL2 只压缩、不加密
		 */
		public static final String LEVEL2 = "L2";
		/**
		 * LEVEL3 先压缩、后加密
		 */
		public static final String LEVEL3 = "L3";
		/**
		 * LEVEL4 先加密、后压缩
		 */
		public static final String LEVEL4 = "L4";
	}
}

相关的资源文件:

#####不变参数
 
client_id=wqch
client_secret=c02397ac7d49417aaa7c
pubKey=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLuDvkKNHs0C0+LHJks/0QVGyLWFSqXPvMEJ6jC17ebft+LMrFvSAcFLiE8TGdFoUyMbqB3XDIK78o/VvoRDhHIGmFJXRs/sfgpPvJPPsAwlHHNFR7Wm1Na/MvqlKnGOVhOTCKzQR8kQ7LUIiYrKHgU+IPtSPa7cA5gMG8YrfAYQIDAQAB
secret_level=L0
 
## 账套code
busi_center=1
 
#################################################################################################
 
#####可变参数
## 获取token方式
grant_type=client_credentials
#grant_type=password
 
#重复校验
#busi_id=1234
#repeat_check=Y
 
## 服务器地址
baseUrl=http://127.0.0.1
 
## 请求参数
requestBody=[{\"code\":\"CNY\"}]
## api访问路径
apiUrl=/nccloud/api/uapbd/currtype/currtype/queryCurrtypeByCode
 

2.3 OpenAPI开发规范

新建业务组件:openapi/src/public
资源包命名规范:nccloud.api.模块.业务组件.资源
示例:nccloud.api.aum.borrow.apply(借用申请)
资源类命名规范:业务组件+资源+Resources(驼峰命名)
示例:BorrowApplyResources
uri定义:nccloud/api/模块/业务组件/资源/动词
(增:add;删除:delete;查询:query;修改:update (其他业务动词自定义))
示例:[http://ip:port/nccloud/api/aum/borrow/apply/query(查询借用申请)。](javascript:void(0))
Md文档参考demo中的格式书写(注:遵循md文档的语法格式)
Md文档的位置:
1、在openapi组件下META-INF同级目录下创建hotwebs文件夹。
2、在资源包的根目录下创建包名为api.modules.模块.组件.md的包
3、在上一步的包下创建相应的md文档

img