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("'); } 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 + "\""; } }