陈昶聿
14 小时以前 36bf25f295b11d6cbebd51473e9288e4afe23c86
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
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("&amp;");
                    break;
                case '<':
                    out.append("&lt;");
                    break;
                case '>':
                    out.append("&gt;");
                    break;
                case '"':
                    out.append("&quot;");
                    break;
                case '\'':
                    out.append("&apos;");
                    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 + "\"";
    }
}