package com.smartor.common;
|
|
import com.smartor.domain.ShiyiSmsRequest;
|
import com.smartor.domain.ShiyiSmsResponse;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.lang3.StringUtils;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.MediaType;
|
import org.springframework.http.ResponseEntity;
|
import org.springframework.stereotype.Component;
|
import org.springframework.web.client.RestTemplate;
|
|
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
import org.w3c.dom.Document;
|
import org.w3c.dom.Element;
|
import org.w3c.dom.Node;
|
import org.w3c.dom.NodeList;
|
import org.xml.sax.InputSource;
|
|
import java.io.StringReader;
|
import java.nio.charset.StandardCharsets;
|
import java.text.SimpleDateFormat;
|
import java.util.Date;
|
|
/**
|
* 杭州市一 HIS 服务平台对接工具类。
|
* <p>
|
* 按《HIS 服务平台接口说明文档 V1.1》3.1 节约定,HIS 对外以 WCF Web Service 形式发布,
|
* 统一入口:{@code int RunService(string TradeType, string TradeMsg, ref string TradeOut)},
|
* 其中 {@code TradeType} 标识业务编码,{@code TradeMsg} 为 UTF-8 编码的 XML 报文。
|
* 本工具类目前实现 5.2 短信业务 (TradeType=FASONGDX)。
|
*/
|
@Slf4j
|
@Component
|
public class ShiyiSmsUtil {
|
|
/** 5.2 短信业务 TradeType */
|
public static final String TRADE_TYPE_FASONGDX = "FASONGDX";
|
|
/** WCF 命名空间,默认随文档示例 */
|
private String namespace = "http://tempuri.org/";
|
|
/** 默认操作员代码 */
|
@Value("${his.service.defaultCaozuoydm:}")
|
private String defaultCaozuoydm;
|
|
/** 默认操作员姓名 */
|
@Value("${his.service.defaultCaozuoyxm:}")
|
private String defaultCaozuoyxm;
|
|
/** 默认系统标识 */
|
private String defaultXitongbs = "0";
|
|
/** 默认分院代码 */
|
private String defaultFenyuandm = "1";
|
|
/** 默认机构代码 */
|
private String defaultJigoudm = "1";
|
|
private final RestTemplate restTemplate = new RestTemplate();
|
|
/**
|
* 发送短信 (FASONGDX)
|
*/
|
public ShiyiSmsResponse sendSms(ShiyiSmsRequest request) {
|
if (request == null) {
|
throw new IllegalArgumentException("短信请求不能为空");
|
}
|
if (StringUtils.isBlank(request.getShoujihao())) {
|
throw new IllegalArgumentException("手机号不能为空");
|
}
|
if (StringUtils.isBlank(request.getDuanxinnr())) {
|
throw new IllegalArgumentException("短信内容不能为空");
|
}
|
|
applyDefaults(request);
|
|
String tradeMsg = buildFasongdxXml(request);
|
log.info("市一 短信请求, TradeType={}, TradeMsg={}", TRADE_TYPE_FASONGDX, tradeMsg);
|
|
String tradeOut = invokeRunService(TRADE_TYPE_FASONGDX, tradeMsg);
|
log.info("市一 短信响应, TradeOut={}", tradeOut);
|
|
return parseFasongdxResponse(tradeOut);
|
}
|
|
/**
|
* 调用 市一 WCF RunService 接口。SOAP 1.1 基础鉴权按文档不要求。
|
*
|
* @return TradeOut 报文(服务端 ref 参数)
|
*/
|
public String invokeRunService(String tradeType, String tradeMsg) {
|
String soapEnvelope = buildSoapEnvelope(tradeType, tradeMsg);
|
String hisServiceUrl = "http://192.200.54.57:7790/MediInfoHis.svc";
|
|
HttpHeaders headers = new HttpHeaders();
|
headers.setContentType(new MediaType("text", "xml", StandardCharsets.UTF_8));
|
headers.set("SOAPAction", joinNamespace(namespace, "IMediInfoHis/RunService"));
|
|
HttpEntity<String> entity = new HttpEntity<>(soapEnvelope, headers);
|
try {
|
ResponseEntity<String> response = restTemplate.postForEntity(hisServiceUrl, entity, String.class);
|
String body = response.getBody();
|
if (StringUtils.isBlank(body)) {
|
throw new RuntimeException("HIS 返回空响应");
|
}
|
return extractTradeOut(body);
|
} catch (Exception e) {
|
log.error("调用 HIS RunService 失败, url={}, tradeType={}, err={}", hisServiceUrl, tradeType, e.getMessage(), e);
|
throw new RuntimeException("调用 HIS RunService 失败: " + e.getMessage(), e);
|
}
|
}
|
|
private void applyDefaults(ShiyiSmsRequest request) {
|
if (StringUtils.isBlank(request.getCaozuoydm())) {
|
request.setCaozuoydm(defaultCaozuoydm);
|
}
|
if (StringUtils.isBlank(request.getCaozuoyxm())) {
|
request.setCaozuoyxm(defaultCaozuoyxm);
|
}
|
if (StringUtils.isBlank(request.getCaozuorq())) {
|
request.setCaozuorq(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
}
|
if (StringUtils.isBlank(request.getXitongbs())) {
|
request.setXitongbs(defaultXitongbs);
|
}
|
if (StringUtils.isBlank(request.getFenyuandm())) {
|
request.setFenyuandm(defaultFenyuandm);
|
}
|
if (StringUtils.isBlank(request.getJigoudm())) {
|
request.setJigoudm(defaultJigoudm);
|
}
|
if (StringUtils.isBlank(request.getDuanxinlx())) {
|
request.setDuanxinlx("0");
|
}
|
}
|
|
private String buildFasongdxXml(ShiyiSmsRequest r) {
|
StringBuilder sb = new StringBuilder(512);
|
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
sb.append("<FASONGDX_IN>");
|
sb.append("<BASEINFO>");
|
appendTag(sb, "CAOZUOYDM", r.getCaozuoydm());
|
appendTag(sb, "CAOZUOYXM", r.getCaozuoyxm());
|
appendTag(sb, "CAOZUORQ", r.getCaozuorq());
|
appendTag(sb, "XITONGBS", r.getXitongbs());
|
appendTag(sb, "FENYUANDM", r.getFenyuandm());
|
appendTag(sb, "JIGOUDM", r.getJigoudm());
|
appendTag(sb, "JIGOUMC", r.getJigoumc());
|
appendTag(sb, "JIGOUYZM", r.getJigouyzm());
|
appendTag(sb, "JIESHOUJGDM", r.getJieshoujgdm());
|
appendTag(sb, "ZHONGDUANJB", r.getZhongduanjb());
|
appendTag(sb, "ZHONGDUANLS", r.getZhongduanls());
|
appendTag(sb, "IPADDRESS", r.getIpaddress());
|
appendTag(sb, "YEWULX", r.getYewulx());
|
appendTag(sb, "JIERUCSDM", r.getJierucsdm());
|
sb.append("</BASEINFO>");
|
appendTag(sb, "DUANXINLX", r.getDuanxinlx());
|
appendTag(sb, "SHOUJIHAO", r.getShoujihao());
|
appendTag(sb, "DUANXINNR", r.getDuanxinnr());
|
sb.append("</FASONGDX_IN>");
|
return sb.toString();
|
}
|
|
private ShiyiSmsResponse parseFasongdxResponse(String xml) {
|
ShiyiSmsResponse resp = new ShiyiSmsResponse();
|
resp.setRawXml(xml);
|
if (StringUtils.isBlank(xml)) {
|
resp.setErrno("-1");
|
resp.setErrmsg("HIS 返回空 TradeOut");
|
return resp;
|
}
|
try {
|
Document doc = parseXml(xml);
|
resp.setErrno(readTag(doc, "ERRNO"));
|
resp.setErrmsg(readTag(doc, "ERRMSG"));
|
resp.setErrmsgex(readTag(doc, "ERRMSGEX"));
|
resp.setMessageId(readTag(doc, "MessageID"));
|
resp.setDuanxinid(readTag(doc, "DUANXINID"));
|
} catch (Exception e) {
|
log.error("解析 HIS 短信响应失败, xml={}, err={}", xml, e.getMessage(), e);
|
resp.setErrno("-1");
|
resp.setErrmsg("解析响应失败: " + e.getMessage());
|
}
|
return resp;
|
}
|
|
private String buildSoapEnvelope(String tradeType, String tradeMsg) {
|
// WCF 默认 BasicHttpBinding 下接口方法以 Message Contract 形式发布,参数节点名与 C# 方法签名一致。
|
// TradeOut 为 ref 参数,入参也需传空元素占位。
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:t=\"" + namespace + "\">"
|
+ "<s:Body>"
|
+ "<t:RunService>"
|
+ "<t:TradeType>" + escapeXml(tradeType) + "</t:TradeType>"
|
+ "<t:TradeMsg>" + escapeXml(tradeMsg) + "</t:TradeMsg>"
|
+ "<t:TradeOut></t:TradeOut>"
|
+ "</t:RunService>"
|
+ "</s:Body>"
|
+ "</s:Envelope>";
|
}
|
|
private String extractTradeOut(String soapResponse) throws Exception {
|
Document doc = parseXml(soapResponse);
|
// TradeOut 节点 (带命名空间)
|
String tradeOut = readTagIgnoreNs(doc, "TradeOut");
|
if (StringUtils.isBlank(tradeOut)) {
|
// 有些 WCF 实现会把 ref 参数输出节点命名为 RunServiceResult 的同级 TradeOut
|
tradeOut = readTagIgnoreNs(doc, "tradeOut");
|
}
|
return tradeOut;
|
}
|
|
private Document parseXml(String xml) throws Exception {
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
factory.setNamespaceAware(false);
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
return builder.parse(new InputSource(new StringReader(xml)));
|
}
|
|
private String readTag(Document doc, String name) {
|
NodeList list = doc.getElementsByTagName(name);
|
if (list.getLength() == 0) {
|
return null;
|
}
|
return list.item(0).getTextContent();
|
}
|
|
private String readTagIgnoreNs(Document doc, String localName) {
|
NodeList all = doc.getElementsByTagName("*");
|
for (int i = 0; i < all.getLength(); i++) {
|
Node node = all.item(i);
|
if (node instanceof Element) {
|
Element el = (Element) node;
|
String local = el.getLocalName() != null ? el.getLocalName() : el.getNodeName();
|
if (local.equalsIgnoreCase(localName) || local.endsWith(":" + localName)) {
|
return el.getTextContent();
|
}
|
if (el.getNodeName().endsWith(":" + localName) || el.getNodeName().equalsIgnoreCase(localName)) {
|
return el.getTextContent();
|
}
|
}
|
}
|
return null;
|
}
|
|
private void appendTag(StringBuilder sb, String tag, String value) {
|
sb.append('<').append(tag).append('>');
|
if (StringUtils.isNotBlank(value)) {
|
sb.append(escapeXml(value));
|
}
|
sb.append("</").append(tag).append('>');
|
}
|
|
private String escapeXml(String input) {
|
if (input == null) {
|
return "";
|
}
|
StringBuilder out = new StringBuilder(input.length());
|
for (int i = 0; i < input.length(); i++) {
|
char c = input.charAt(i);
|
switch (c) {
|
case '&':
|
out.append("&");
|
break;
|
case '<':
|
out.append("<");
|
break;
|
case '>':
|
out.append(">");
|
break;
|
case '"':
|
out.append(""");
|
break;
|
case '\'':
|
out.append("'");
|
break;
|
default:
|
out.append(c);
|
}
|
}
|
return out.toString();
|
}
|
|
private String joinNamespace(String ns, String op) {
|
if (ns == null) {
|
return "\"" + op + "\"";
|
}
|
if (ns.endsWith("/")) {
|
return "\"" + ns + op + "\"";
|
}
|
return "\"" + ns + "/" + op + "\"";
|
}
|
}
|