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 服务平台对接工具类。
*
* 按《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 entity = new HttpEntity<>(soapEnvelope, headers);
try {
ResponseEntity 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("");
sb.append("");
sb.append("");
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("");
appendTag(sb, "DUANXINLX", r.getDuanxinlx());
appendTag(sb, "SHOUJIHAO", r.getShoujihao());
appendTag(sb, "DUANXINNR", r.getDuanxinnr());
sb.append("");
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 ""
+ ""
+ ""
+ ""
+ "" + escapeXml(tradeType) + ""
+ "" + escapeXml(tradeMsg) + ""
+ ""
+ ""
+ ""
+ "";
}
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 + "\"";
}
}