广西医保两定接口 V4.2 — Java 技术实现文档(银海医保)
# 广西医保两定接口 V4.2 — Java 技术实现文档

> 基于《广西壮族自治区医疗保障信息平台两定医疗机构接口接入指引V4.2》
> 本文档为 **Java 专属实现方案**,完整覆盖所有业务流程
---
## 目录
1. [技术架构与选型](#1-技术架构与选型)
2. [开发环境搭建](#2-开发环境搭建)
3. [项目结构](#3-项目结构)
4. [数据模型层(Model)](#4-数据模型层model)
5. [配置管理(ConfigManager)](#5-配置管理configmanager)
6. [日志工具(Logger)](#6-日志工具logger)
7. [COM DLL调用层(YhComClient)](#7-com-dll调用层yhcomclient)
8. [HTTP API调用层(ApiClient)](#8-http-api调用层apiclient)
9. [门诊结算完整流程实现](#9-门诊结算完整流程实现)
10. [住院结算完整流程实现](#10-住院结算完整流程实现)
11. [冲正交易实现](#11-冲正交易实现)
12. [目录下载实现](#12-目录下载实现)
13. [费用对账实现](#13-费用对账实现)
14. [Swing主界面实现](#14-swing主界面实现)
15. [程序入口与打包运行](#15-程序入口与打包运行)
16. [常见问题排查](#16-常见问题排查)
---
## 1. 技术架构与选型
### 1.1 系统交互关系
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ HIS系统 │────▶│ Java客户端 │────▶│ 医保信息平台 │
│ (医院信息) │ │ (本程序) │ │ (医保专网/政务外网) │
└─────────────┘ └──────────────────┘ └─────────────────────┘
│ │
┌──────┘ └──────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ COM DLL组件 │ │ HTTP API服务 │
│(Jacob库桥接) │ │ (Java HttpClient) │
│yh_interface │ │ POST JSON │
│ _xyb.dll │ │ │
└──────────────┘ └──────────────────┘
```
### 1.2 技术栈
| 层次 | 技术 | 说明 |
|------|------|------|
| 语言 | Java 11+ | 使用HttpClient等现代API |
| GUI | Java Swing | JDK内置,无需额外依赖 |
| COM桥接 | **Jacob 1.21** | Java调COM组件的标准库 |
| JSON | **Jackson 2.15** | JSON序列化/反序列化 |
| INI读写 | **ini4j 0.5.4** | 读取yhinterfacenew.ini |
| 日志 | **Logback 1.4** | SLF4J实现,控制台+文件 |
| 构建 | **Maven 3.x** | 项目构建和依赖管理 |
### 1.3 两种调用通道
| 通道 | 用途 | 交易编号 | Java调用方式 |
|------|------|---------|-------------|
| **COM DLL** | 读卡/刷卡 | 1401 | Jacob `ActiveXComponent` + `Dispatch` |
| **HTTP API** | 所有业务交易 | 1101, 2201-2404等 | `java.net.http.HttpClient` |
### 1.4 业务流程总览
```
读卡(1401,DLL/Jacob) → 人员信息(1101,HTTP) → 业务操作 → 结算 → 打印单据
```
---
## 2. 开发环境搭建
### 2.1 JDK安装
需要 **JDK 11** 或更高版本(推荐JDK 17 LTS):
```bash
# 验证安装
java -version
# 预期输出: openjdk version "11.x" 或更高
```
### 2.2 Maven安装
```bash
mvn -version
# 预期输出: Apache Maven 3.x.x
```
### 2.3 Jacob库安装(关键步骤)
Jacob是Java调COM组件的桥梁,需要两部分:
**① jacob.jar** — Maven自动下载(pom.xml已配置)
**② jacob-1.21-x86.dll**(或x64)— 需手动放到系统PATH中:
```batch
:: 方法一:放到Windows系统目录
copy jacob-1.21-x86.dll C:\Windows\System32\
:: 方法二:放到项目运行目录
copy jacob-1.21-x86.dll d:\yinhaisoftnew\
:: 方法三:通过JVM参数指定
java -Djava.library.path=d:\yinhaisoftnew -jar xxx.jar
```
**选择x86还是x64?** 取决于JDK位数:
- 32位JDK → `jacob-1.21-x86.dll`
- 64位JDK → `jacob-1.21-x64.dll`
### 2.4 COM组件注册
必须先运行 `regsvr32.bat`(管理员权限):
```batch
regsvr32 D:\yinhaisoftnew\yh_interface_xyb.dll
regsvr32 D:\yinhaisoftnew\exapp.dll
regsvr32 D:\yinhaisoftnew\MSWINSCK.OCX
```
### 2.5 必需运行文件
以下文件必须存在于Java程序的工作目录中:
| 文件 | 说明 |
|------|------|
| `yh_interface_xyb.dll` | 主COM组件(regsvr32.bat中注册) |
| `yh_interfaceproxy_new.dll` | 接口代理DLL |
| `yh_local_wlmq_new.dll` | 本地网络通信组件 |
| `yh_readcard.dll` | 读卡功能DLL |
| `yhinterfacenew.ini` | 核心配置文件 |
| `config.ini` | 读卡器硬件配置 |
| `jacob-1.21-x86.dll` | Jacob本地库 |
---
## 3. 项目结构
### 3.1 Maven项目完整结构
```
MedicalInsuranceInterfaceJava/
├── pom.xml # Maven构建配置
└── src/
└── main/
├── java/com/yinhai/medical/
│ ├── App.java # 程序入口
│ ├── model/ # 数据模型层
│ │ ├── ApiRequestHeader.java # HTTP请求报文头
│ │ ├── ApiResponseMessage.java # HTTP响应报文
│ │ ├── AppConfig.java # 应用配置
│ │ └── CardReadResult.java # 读卡结果(含10分钟有效期)
│ ├── service/ # 服务层
│ │ ├── ConfigManager.java # INI配置读写
│ │ ├── Logger.java # 日志工具(控制台+文件+UI)
│ │ ├── YhComClient.java # COM DLL客户端(Jacob)
│ │ └── ApiClient.java # HTTP API客户端(全部业务方法)
│ └── ui/ # 界面层
│ └── MainForm.java # Swing主界面(9个Tab页)
└── resources/
└── logback.xml # Logback日志配置
```
### 3.2 pom.xml 完整配置
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yinhai</groupId>
<artifactId>medical-insurance-interface</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>医保两定接口客户端</name>
<description>广西壮族自治区医疗保障信息平台两定接口Java客户端</description>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Jacob: Java COM Bridge,调用yh_interface_xyb.dll -->
<dependency>
<groupId>net.sf.jacob-project</groupId>
<artifactId>jacob</artifactId>
<version>1.21</version>
</dependency>
<!-- Jackson: JSON序列化/反序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Logback: 日志框架 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<!-- ini4j: INI配置文件读写 -->
<dependency>
<groupId>org.ini4j</groupId>
<artifactId>ini4j</artifactId>
<version>0.5.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 设置主类 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.yinhai.medical.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<!-- 打包为可执行的fat-jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.yinhai.medical.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
```
---
## 4. 数据模型层(Model)
### 4.1 ApiRequestHeader.java — HTTP请求报文头
对应文档表1的所有字段,使用Jackson注解映射JSON字段名。
```java
package com.yinhai.medical.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* API接口请求报文头(对应文档表1)
*
* 所有HTTP API请求都使用此统一报文头格式。
* 关键字段:infno(交易编号)、msgid(报文ID)、input(交易输入)
*/
public class ApiRequestHeader {
@JsonProperty("infno")
private String infNo; // 交易编号(4位,必填)
@JsonProperty("msgid")
private String msgId; // 发送方报文ID(30位,必填)
@JsonProperty("mdtrtarea_admvs")
private String mdtrtAreaAdmvs; // 就医地医保区划(6位,必填)
@JsonProperty("insuplc_admdvs")
private String insuplcAdmdvs; // 参保地医保区划(6位,必填)
@JsonProperty("recer_sys_code")
private String recerSysCode = "fsi"; // 接收方系统代码(固定fsi)
@JsonProperty("dev_no")
private String devNo = ""; // 设备编号(暂未启用)
@JsonProperty("dev_safe_info")
private String devSafeInfo = ""; // 设备安全信息(暂未启用)
@JsonProperty("cainfo")
private String caInfo = ""; // 数字签名信息(暂未启用)
@JsonProperty("signtype")
private String signType = ""; // 签名类型(暂未启用)
@JsonProperty("infver")
private String infVer = "V1.0"; // 接口版本号(必填)
@JsonProperty("opter_type")
private String opterType = "1"; // 经办人类别(1经办人/2自助/3移动)
@JsonProperty("opter")
private String opter; // 经办人工号(必填)
@JsonProperty("opter_name")
private String opterName; // 经办人姓名(必填)
@JsonProperty("inf_time")
private String infTime; // 交易时间(必填)
@JsonProperty("fixmedins_code")
private String fixMedInsCode; // 定点医药机构编号(12位,必填)
@JsonProperty("fixmedins_name")
private String fixMedInsName; // 定点医药机构名称(必填)
@JsonProperty("sign_no")
private String signNo = ""; // 签到流水号(通过9001获取)
@JsonProperty("oinfno")
private String oInfNo = ""; // 原交易编号(冲正时使用)
@JsonProperty("omsgid")
private String oMsgId = ""; // 原发送方报文ID(冲正时使用)
@JsonProperty("input")
private Object input; // 交易输入(JSON对象)
/**
* 生成发送方报文ID
* 规则:机构编号(12) + 时间(14) + 顺序号(4) = 30位
* 示例:H4500000012 + 20250101120000 + 0001
*/
public static String generateMsgId(String fixMedInsCode, int seqNo) {
String time = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
return String.format("%s%s%04d", fixMedInsCode, time, seqNo);
}
// ===== Getters and Setters (省略,完整代码见源码文件) =====
public String getInfNo() { return infNo; }
public void setInfNo(String v) { this.infNo = v; }
public String getMsgId() { return msgId; }
public void setMsgId(String v) { this.msgId = v; }
// ... 其余getter/setter见实际源码
public Object getInput() { return input; }
public void setInput(Object input) { this.input = input; }
}
```
### 4.2 ApiResponseMessage.java — HTTP响应报文
```java
package com.yinhai.medical.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* API接口响应报文
* infcode=0 表示成功,-1 表示失败
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class ApiResponseMessage {
@JsonProperty("infcode")
private int infCode; // 0成功, -1失败
@JsonProperty("inf_refmsgid")
private String infRefMsgId; // 接收方报文ID
@JsonProperty("refmsg_time")
private String refMsgTime; // 接收报文时间
@JsonProperty("respond_time")
private String respondTime; // 响应报文时间
@JsonProperty("err_msg")
private String errMsg; // 错误信息(失败时返回)
@JsonProperty("warn_msg")
private String warnMsg; // 警告信息
@JsonProperty("output")
private Object output; // 交易输出(JSON对象)
@JsonProperty("signtype")
private String signType;
@JsonProperty("cainfo")
private String caInfo;
/** 判断交易是否成功 */
public boolean isSuccess() { return infCode == 0; }
// Getters and Setters...
public int getInfCode() { return infCode; }
public void setInfCode(int v) { this.infCode = v; }
public String getErrMsg() { return errMsg; }
public void setErrMsg(String v) { this.errMsg = v; }
public Object getOutput() { return output; }
public void setOutput(Object v) { this.output = v; }
// ... 其余见源码
}
```
### 4.3 AppConfig.java — 应用配置
```java
package com.yinhai.medical.model;
/**
* 应用配置(从yhinterfacenew.ini读取)
*
* 关键字段说明:
* - fixMedInsCode: 贯标后H或P开头的12位机构编码
* - mdtrtAreaAdmvs: 就医地6位行政区划代码
* - comProgId: COM组件ProgramID,固定为YinHai.XJ.Actual.Interface.New
*/
public class AppConfig {
private String fixMedInsCode = ""; // 机构编号
private String fixMedInsName = ""; // 机构名称
private String mdtrtAreaAdmvs = ""; // 就医地医保区划(6位)
private String apiBaseUrl = ""; // 医保业务接口URL
private String comProgId = "YinHai.XJ.Actual.Interface.New"; // COM组件ID
private String opterId = "001"; // 经办人工号
private String opterName = "系统管理员"; // 经办人姓名
private String signNo = ""; // 交易签到流水号
private int seqCounter = 1; // 报文顺序号计数器
/**
* 获取下一个报文顺序号(线程安全)
* 顺序号范围:1-9999,循环使用
*/
public synchronized int nextSeqNo() {
int seq = seqCounter;
seqCounter = (seqCounter % 9999) + 1;
return seq;
}
// Getters and Setters...
}
```
### 4.4 CardReadResult.java — 读卡结果
```java
package com.yinhai.medical.model;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 读卡返回的参保人信息缓存
*
* 关键规则:安全认证有效期为10分钟(600秒)
* 超时后必须重新读卡,否则后续交易会被拒绝
*/
public class CardReadResult {
private String certNo; // 证件号码 → 传给1101
private String psnName; // 姓名 → 显示用
private String cardNo; // 社保卡号 → 社保卡时返回
private String insuplcAdmdvs; // 参保地分中心 → 填入报文头
private String cardSn; // 卡识别码 → 填入1101的card_sn
private String psnCertType; // 证件类型 → 填入1101的psn_cert_type
private String mdtrtCertNo; // 就诊凭证编号 → ★结算必传
private String mdtrtCertType; // 就诊凭证类型 → ★结算必传
private String ecToken; // 凭证令牌 → 电子凭证返回
private LocalDateTime readTime; // 读卡时间
/**
* 检查读卡信息是否已过期(有效期10分钟=600秒)
* 门诊/住院/药店结算前必须检查此状态
*/
public boolean isExpired() {
if (readTime == null) return true;
return Duration.between(readTime, LocalDateTime.now()).getSeconds() > 600;
}
// Getters and Setters...
public String getMdtrtCertNo() { return mdtrtCertNo; }
public void setMdtrtCertNo(String v) { this.mdtrtCertNo = v; }
public String getMdtrtCertType() { return mdtrtCertType; }
public void setMdtrtCertType(String v) { this.mdtrtCertType = v; }
// ... 其余见源码
}
```
---
## 5. 配置管理(ConfigManager)
```java
package com.yinhai.medical.service;
import com.yinhai.medical.model.AppConfig;
import org.ini4j.Ini;
import java.io.File;
/**
* 配置管理器 - 读取/保存 yhinterfacenew.ini
*
* yhinterfacenew.ini 是核心配置文件,结构如下:
* [parameter]
* yab278=4507 ← 就医地医保区划
* akb020=H4500000012 ← 医疗机构编号
* pid=YinHai.XJ.Actual.Interface.New ← COM组件ID
* [wlmq]
* akb021=某某医院 ← 机构名称
* url=http://... ← 医保接口URL
*/
public class ConfigManager {
private static final String BASE_DIR = System.getProperty("user.dir");
/**
* 从yhinterfacenew.ini加载配置
*/
public static AppConfig loadConfig() {
AppConfig config = new AppConfig();
File iniFile = new File(BASE_DIR, "yhinterfacenew.ini");
if (iniFile.exists()) {
try {
Ini ini = new Ini(iniFile);
// 读取 [parameter] 节
Ini.Section param = ini.get("parameter");
if (param != null) {
config.setMdtrtAreaAdmvs(getVal(param, "yab278", ""));
config.setFixMedInsCode(getVal(param, "akb020", ""));
config.setComProgId(getVal(param, "pid",
"YinHai.XJ.Actual.Interface.New"));
}
// 读取 [wlmq] 节
Ini.Section wlmq = ini.get("wlmq");
if (wlmq != null) {
String url = getVal(wlmq, "url", "");
if (!url.isEmpty()) config.setApiBaseUrl(url);
config.setFixMedInsName(getVal(wlmq, "akb021", "机构名称"));
config.setEcUrl(getVal(wlmq, "ecurl", ""));
}
} catch (Exception e) {
Logger.error("读取配置文件失败", e);
}
}
return config;
}
/**
* 保存配置到yhinterfacenew.ini
*/
public static void saveConfig(AppConfig config) {
File iniFile = new File(BASE_DIR, "yhinterfacenew.ini");
try {
Ini ini = iniFile.exists() ? new Ini(iniFile) : new Ini();
Ini.Section param = ini.get("parameter");
if (param == null) param = ini.add("parameter");
param.put("yab278", config.getMdtrtAreaAdmvs());
param.put("akb020", config.getFixMedInsCode());
param.put("pid", config.getComProgId());
Ini.Section wlmq = ini.get("wlmq");
if (wlmq == null) wlmq = ini.add("wlmq");
wlmq.put("akb020", config.getFixMedInsCode());
wlmq.put("akb021", config.getFixMedInsName());
wlmq.put("url", config.getApiBaseUrl());
ini.store(iniFile);
Logger.info("配置已保存到 yhinterfacenew.ini");
} catch (Exception e) {
Logger.error("保存配置文件失败", e);
}
}
private static String getVal(Ini.Section section, String key, String def) {
String val = section.get(key);
return val != null ? val.trim() : def;
}
}
```
---
## 6. 日志工具(Logger)
```java
package com.yinhai.medical.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Consumer;
/**
* 日志工具类 - 三路输出:控制台 + 文件 + UI
*
* 特点:
* - setUiLogConsumer() 设置回调,将日志同步显示到Swing界面
* - 自动格式化JSON报文,便于调试阅读
* - 日志文件按日期滚动:log_yyyyMMdd.txt
*/
public class Logger {
private static final String LOG_DIR = Paths.get(
System.getProperty("user.dir"), "Logs").toString();
private static final DateTimeFormatter TIME_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private static final DateTimeFormatter DATE_FMT =
DateTimeFormatter.ofPattern("yyyyMMdd");
private static final ObjectMapper MAPPER =
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
// UI日志回调持有者
private static final Consumer<String>[] UI_HOLDER = new Consumer[1];
/** 设置UI日志回调 */
public static void setUiLogConsumer(Consumer<String> consumer) {
UI_HOLDER[0] = consumer;
}
public static void info(String message) {
writeLog("INFO", message);
}
public static void error(String message, Throwable t) {
String msg = t != null ? message + "\n异常:" + t.getMessage() : message;
writeLog("ERROR", msg);
}
public static void error(String message) {
writeLog("ERROR", message);
}
/** 记录请求报文(自动格式化JSON) */
public static void logRequest(String transNo, String requestJson) {
writeLog("REQUEST", "交易编号: " + transNo +
"\n请求报文:\n" + formatJson(requestJson));
}
/** 记录响应报文(自动格式化JSON) */
public static void logResponse(String transNo, String responseJson) {
writeLog("RESPONSE", "交易编号: " + transNo +
"\n响应报文:\n" + formatJson(responseJson));
}
private static void writeLog(String level, String message) {
String logLine = String.format("[%s] [%s] %s",
LocalDateTime.now().format(TIME_FMT), level, message);
// 1. 输出到控制台
System.out.println(logLine);
// 2. 输出到UI(如果设置了回调)
if (UI_HOLDER[0] != null) {
String shortTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
UI_HOLDER[0].accept(
String.format("[%s] %s", shortTime, message));
}
// 3. 写入文件
try {
Path logDirPath = Paths.get(LOG_DIR);
if (!Files.exists(logDirPath))
Files.createDirectories(logDirPath);
String logFile = Paths.get(LOG_DIR,
"log_" + LocalDateTime.now().format(DATE_FMT) + ".txt")
.toString();
try (Writer w = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(logFile, true),
StandardCharsets.UTF_8))) {
w.write(logLine);
w.write(System.lineSeparator());
}
} catch (IOException ignored) {
// 日志写入失败不影响主流程
}
}
private static String formatJson(String json) {
if (json == null || json.isEmpty()) return json;
try {
Object obj = MAPPER.readValue(json, Object.class);
return MAPPER.writeValueAsString(obj);
} catch (Exception e) {
return json;
}
}
}
```
---
## 7. COM DLL调用层(YhComClient)
这是Java调用银海COM组件的核心类,使用Jacob库作为桥梁。
### 7.1 Jacob调用原理
```
Java代码 → Jacob.jar → jacob.dll → COM组件(yh_interface_xyb.dll)
↓
gxyb_call()
↓
返回JSON结果
```
### 7.2 完整源码
```java
package com.yinhai.medical.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.yinhai.medical.model.AppConfig;
import com.yinhai.medical.model.CardReadResult;
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 银海COM DLL客户端
*
* 调用目标: yh_interface_xyb.dll (ProgramID: YinHai.XJ.Actual.Interface.New)
* 调用函数: gxyb_call(string transNo, string inputJson, ref string outputJson)
*
* Jacob关键技术点:
* 1. ActiveXComponent 创建COM实例(等同PB的ConnectToNewObject)
* 2. Dispatch.callSub 调用COM方法
* 3. Variant("", true) 创建byRef参数(对应PB的ref关键字)
* 4. 从byRef参数获取输出: args[2].toString()
*/
public class YhComClient implements AutoCloseable {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final DateTimeFormatter TS_FMT =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private ActiveXComponent comObject;
private boolean connected = false;
private final String progId;
public YhComClient() {
this("YinHai.XJ.Actual.Interface.New");
}
public YhComClient(String progId) {
this.progId = progId;
}
/**
* 连接COM组件
* 对应PB: yinhaiobject = create oleobject
* gi_result = yinhaiobject.ConnectToNewObject(
* "YinHai.XJ.Actual.Interface.New")
*/
public boolean connect() {
try {
comObject = new ActiveXComponent(progId);
connected = true;
Logger.info("成功连接COM组件: " + progId);
return true;
} catch (Exception e) {
Logger.error("连接COM组件失败: " + progId, e);
connected = false;
return false;
}
}
public boolean isConnected() { return connected; }
/**
* 调用交易函数 gxyb_call
*
* PB调用方式:
* yinhaiobject.gxyb_call('1401', inputJson, ref outputJson)
*
* Java+Jacob方式:
* Dispatch.callSub(comObject, "gxyb_call", args)
* 其中args[2]是byRef的Variant,调用后包含输出值
*/
public String call(String transNo, String inputJson) {
if (!connected || comObject == null) {
throw new IllegalStateException(
"COM组件未连接,请先调用connect()");
}
Logger.logRequest(transNo, inputJson);
try {
// 构建参数数组
Variant[] args = new Variant[]{
new Variant(transNo), // 第1个参数: 交易编号
new Variant(inputJson), // 第2个参数: 交易输入JSON
new Variant("", true) // 第3个参数: byRef输出参数
};
// 调用COM方法
Dispatch.callSub(comObject, "gxyb_call", args);
// 从byRef参数获取输出结果
String outputJson = args[2].toString();
Logger.logResponse(transNo, outputJson);
return outputJson;
} catch (Exception e) {
Logger.error("调用DLL交易[" + transNo + "]失败", e);
throw new RuntimeException(
"调用DLL交易[" + transNo + "]失败: " + e.getMessage(), e);
}
}
/**
* 调用1401读卡交易
*
* 流程:弹出读卡界面 → 选择凭证类型 → 返回参保人信息
* 支持:医保电子凭证、电子社保卡、社会保障卡、身份证
* 有效期:10分钟(600秒)
*/
public CardReadResult readCard(AppConfig config, String businessType) {
// 构建1401请求JSON
ObjectNode root = MAPPER.createObjectNode();
root.put("transNo", "1401");
root.put("transType", "passwordCheck");
root.put("timestamp", LocalDateTime.now().format(TS_FMT));
ObjectNode data = root.putObject("data");
data.put("fixmedinsCode", config.getFixMedInsCode());
data.put("psnNo", "");
data.put("businessType", businessType);
data.put("opterId", config.getOpterId());
data.put("opterName", config.getOpterName());
data.put("deptId", "0001");
data.put("deptName", "综合科室");
data.put("deviceType", "0");
String inputJson;
try {
inputJson = MAPPER.writeValueAsString(root);
} catch (Exception e) {
throw new RuntimeException("构建读卡请求失败", e);
}
// 调用DLL
String outputJson = call("1401", inputJson);
// 解析响应
try {
JsonNode response = MAPPER.readTree(outputJson);
// 检查infcode(0为成功)
int infcode = response.path("infcode").asInt(-1);
if (infcode != 0) {
String errMsg = response.path("err_msg")
.asText("读卡失败,未知错误");
throw new RuntimeException("读卡失败: " + errMsg);
}
// 解析output中的参保人信息
JsonNode output = response.path("output");
CardReadResult result = new CardReadResult();
result.setCertNo(output.path("certno").asText(""));
result.setPsnName(output.path("psn_name").asText(""));
result.setCardNo(output.path("card_no").asText(""));
result.setInsuplcAdmdvs(
output.path("insuplc_admdvs").asText(""));
result.setCardSn(output.path("card_sn").asText(""));
result.setPsnCertType(
output.path("psn_cert_type").asText(""));
result.setMdtrtCertNo(
output.path("mdtrt_cert_no").asText(""));
result.setMdtrtCertType(
output.path("mdtrt_cert_type").asText(""));
result.setEcToken(output.path("ecToken").asText(""));
result.setReadTime(LocalDateTime.now());
Logger.info(String.format(
"读卡成功: 姓名=%s, 证件号=%s, 凭证类型=%s",
result.getPsnName(), result.getCertNo(),
result.getMdtrtCertType()));
return result;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("解析读卡响应失败", e);
}
}
@Override
public void close() {
if (comObject != null) {
try { comObject.safeRelease(); } catch (Exception ignored) {}
comObject = null;
connected = false;
}
}
}
```
---
## 8. HTTP API调用层(ApiClient)
### 8.1 交易列表
| 交易编号 | 名称 | Java方法名 |
|---------|------|-----------|
| 9001 | 交易签到 | `signIn()` |
| 1101 | 人员信息获取 | `getPersonInfo()` |
| 2001 | 待遇享受检查 | `checkBenefit()` |
| 2201 | 门诊挂号 | `outpatientRegister()` |
| 2202 | 门诊挂号撤销 | `outpatientRegisterCancel()` |
| 2203 | 门诊就诊信息上传 | `uploadOutpatientVisit()` |
| 2204 | 门诊费用明细上传 | `uploadOutpatientFeeDetail()` |
| 2207 | **门诊结算** | `outpatientSettle()` |
| 2208 | 门诊结算撤销 | `outpatientSettleCancel()` |
| 2401 | **入院办理** | `inpatientAdmission()` |
| 2402 | 住院费用明细上传 | `uploadInpatientFeeDetail()` |
| 2403 | 住院信息变更 | `inpatientInfoChange()` |
| 2404 | 出院办理 | `inpatientDischarge()` |
| 2304 | **住院结算** | `inpatientSettle()` |
| 2305 | 住院结算撤销 | `inpatientSettleCancel()` |
| 4101 | 结算清单上传 | `uploadSettlementList()` |
| P0001 | 结算单据打印 | `printSettlement()` |
| 13xx | 目录下载 | `downloadCatalog()` |
| 9102 | 文件下载 | `downloadFile()` |
| 5201 | 对总账 | `reconciliationTotal()` |
### 8.2 核心发送方法
```java
package com.yinhai.medical.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.yinhai.medical.model.*;
import java.net.URI;
import java.net.http.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 医保API HTTP客户端
*
* 请求方式: POST
* MIME类型: Content-Type: application/json
* 超时时间: 60秒
*/
public class ApiClient implements AutoCloseable {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final DateTimeFormatter TIME_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final HttpClient httpClient;
private final AppConfig config;
public ApiClient(AppConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)).build();
}
/**
* 构建请求报文头(统一格式)
*/
private ApiRequestHeader buildHeader(String transNo,
Object input, String insuplcAdmdvs) {
int seqNo = config.nextSeqNo();
ApiRequestHeader h = new ApiRequestHeader();
h.setInfNo(transNo);
h.setMsgId(ApiRequestHeader.generateMsgId(
config.getFixMedInsCode(), seqNo));
h.setMdtrtAreaAdmvs(config.getMdtrtAreaAdmvs());
h.setInsuplcAdmdvs(insuplcAdmdvs != null ?
insuplcAdmdvs : config.getMdtrtAreaAdmvs());
h.setInfVer("V1.0");
h.setOpterType("1");
h.setOpter(config.getOpterId());
h.setOpterName(config.getOpterName());
h.setInfTime(LocalDateTime.now().format(TIME_FMT));
h.setFixMedInsCode(config.getFixMedInsCode());
h.setFixMedInsName(config.getFixMedInsName());
h.setSignNo(config.getSignNo());
h.setInput(input);
return h;
}
/**
* 发送API交易(核心方法)
* 所有业务交易最终都调用此方法
*/
public ApiResponseMessage send(String transNo,
Object input, String insuplcAdmdvs) {
ApiRequestHeader header =
buildHeader(transNo, input, insuplcAdmdvs);
try {
String requestJson = MAPPER.writeValueAsString(header);
Logger.logRequest(transNo, requestJson);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getApiBaseUrl()))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(60))
.POST(HttpRequest.BodyPublishers
.ofString(requestJson))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
String responseJson = response.body();
Logger.logResponse(transNo, responseJson);
return MAPPER.readValue(responseJson,
ApiResponseMessage.class);
} catch (java.net.http.HttpTimeoutException e) {
Logger.error("交易[" + transNo + "]超时");
return errorResp("交易[" + transNo + "]请求超时");
} catch (Exception e) {
Logger.error("交易[" + transNo + "]调用失败", e);
return errorResp("交易[" + transNo + "]异常: "
+ e.getMessage());
}
}
public ApiResponseMessage send(String transNo, Object input) {
return send(transNo, input, null);
}
private ApiResponseMessage errorResp(String msg) {
ApiResponseMessage r = new ApiResponseMessage();
r.setInfCode(-1);
r.setErrMsg(msg);
return r;
}
```
### 8.3 各业务交易方法
```java
// ========== 【1101】人员信息获取 ==========
/**
* 使用读卡返回的凭证信息获取参保人医保信息
* 返回:个人编号(psn_no)、险种(insutype)、余额(balc)等
*/
public ApiResponseMessage getPersonInfo(CardReadResult card) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_cert_no", card.getMdtrtCertNo());
data.put("mdtrt_cert_type", card.getMdtrtCertType());
data.put("psn_cert_type", card.getPsnCertType());
data.put("certno", card.getCertNo());
data.put("card_sn", card.getCardSn());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("1101", input, card.getInsuplcAdmdvs());
}
// ========== 【2201】门诊挂号 ==========
public ApiResponseMessage outpatientRegister(
String psnNo, String insutype,
String insuplcAdmdvs, String mdtrtCertNo,
String mdtrtCertType, String mdtrtId,
String deptCode, String deptName, String drtCode) {
ObjectNode data = MAPPER.createObjectNode();
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrt_cert_no", mdtrtCertNo);
data.put("mdtrt_cert_type", mdtrtCertType);
data.put("mdtrt_id", mdtrtId);
data.put("dept_code", deptCode);
data.put("dept_name", deptName);
data.put("drt_code", drtCode);
data.put("regn_time", LocalDateTime.now().format(TIME_FMT));
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2201", input, insuplcAdmdvs);
}
// ========== 【2203】门诊就诊信息上传 ==========
public ApiResponseMessage uploadOutpatientVisit(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, String mdtrtCertNo,
String mdtrtCertType, String diagCode,
String diagName, String deptCode, String deptName) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrt_cert_no", mdtrtCertNo);
data.put("mdtrt_cert_type", mdtrtCertType);
data.put("maindiag_code", diagCode);
data.put("maindiag_name", diagName);
data.put("dept_code", deptCode);
data.put("dept_name", deptName);
data.put("mdtrt_time",
LocalDateTime.now().format(TIME_FMT));
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2203", input, insuplcAdmdvs);
}
// ========== 【2204】门诊费用明细上传 ==========
/**
* 每批次不超过100条明细
* 费用计算规则:数量×单价 四舍五入保留两位小数 = 总额
*/
public ApiResponseMessage uploadOutpatientFeeDetail(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, ArrayNode feeDetails) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
data.set("feedetail", feeDetails);
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2204", input, insuplcAdmdvs);
}
// ========== 【2207】门诊结算 ==========
/**
* 门诊结算 - 传入的总额必须等于明细相加的总额
*/
public ApiResponseMessage outpatientSettle(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, String mdtrtCertNo,
String mdtrtCertType, String medfeeSumamt,
String psnCertType, String certno) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrt_cert_no", mdtrtCertNo);
data.put("mdtrt_cert_type", mdtrtCertType);
data.put("psn_cert_type", psnCertType);
data.put("certno", certno);
data.put("medfee_sumamt", medfeeSumamt);
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
data.put("med_type", "11");
data.put("mdtrt_time",
LocalDateTime.now().format(TIME_FMT));
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2207", input, insuplcAdmdvs);
}
// ========== 【2208】门诊结算撤销 ==========
public ApiResponseMessage outpatientSettleCancel(
String setlId, String insuplcAdmdvs) {
ObjectNode data = MAPPER.createObjectNode();
data.put("setl_id", setlId);
data.put("rvst_type", "1");
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2208", input, insuplcAdmdvs);
}
// ========== 【2401】入院办理 ==========
/**
* 注意:待遇检查(2001)不可强制与入院登记绑定!
* 跨省异地时国家平台没有匹配的异地2001交易
*/
public ApiResponseMessage inpatientAdmission(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, String mdtrtCertNo,
String mdtrtCertType, String deptCode,
String deptName, String drtCode,
String admDate, String mainDiagCode) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrt_cert_no", mdtrtCertNo);
data.put("mdtrt_cert_type", mdtrtCertType);
data.put("dept_code", deptCode);
data.put("dept_name", deptName);
data.put("drt_code", drtCode);
data.put("adm_date", admDate);
data.put("maindiag_code", mainDiagCode);
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2401", input, insuplcAdmdvs);
}
// ========== 【2402】住院费用明细上传 ==========
public ApiResponseMessage uploadInpatientFeeDetail(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, ArrayNode feeDetails) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
data.set("feedetail", feeDetails);
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2402", input, insuplcAdmdvs);
}
// ========== 【2404】出院办理 ==========
public ApiResponseMessage inpatientDischarge(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, String disDate,
String disDiagCode) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("dis_date", disDate);
data.put("maindiag_code", disDiagCode);
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2404", input, insuplcAdmdvs);
}
// ========== 【2304】住院结算 ==========
public ApiResponseMessage inpatientSettle(
String mdtrtId, String psnNo, String insutype,
String insuplcAdmdvs, String mdtrtCertNo,
String mdtrtCertType, String medfeeSumamt,
String psnCertType, String certno) {
ObjectNode data = MAPPER.createObjectNode();
data.put("mdtrt_id", mdtrtId);
data.put("psn_no", psnNo);
data.put("insutype", insutype);
data.put("mdtrt_cert_no", mdtrtCertNo);
data.put("mdtrt_cert_type", mdtrtCertType);
data.put("psn_cert_type", psnCertType);
data.put("certno", certno);
data.put("medfee_sumamt", medfeeSumamt);
data.put("mdtrtarea_admvs", config.getMdtrtAreaAdmvs());
data.put("fixmedins_code", config.getFixMedInsCode());
data.put("med_type", "21");
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2304", input, insuplcAdmdvs);
}
// ========== 【2305】住院结算撤销 ==========
public ApiResponseMessage inpatientSettleCancel(
String setlId, String insuplcAdmdvs) {
ObjectNode data = MAPPER.createObjectNode();
data.put("setl_id", setlId);
data.put("rvst_type", "1");
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("2305", input, insuplcAdmdvs);
}
```
---
## 9. 门诊结算完整流程实现
### 9.1 流程步骤
```
[1] 读卡(1401,Jacob/DLL) → 获取mdtrt_cert_no等
[2] 人员信息(1101,HTTP) → 获取psn_no、insutype
[3] 门诊挂号(2201,HTTP) → 获取mdtrt_id
[4] 上传就诊(2203,HTTP) → 上传诊断信息
[5] 上传费用(2204,HTTP) → 每批≤100条
[6] 门诊结算(2207,HTTP) → 核心步骤
[7] 结算清单(4101,HTTP) → 结算后必传
[8] 打印单据(P0001,DLL) → 可选
```
### 9.2 Swing界面中的调用代码
```java
// 在MainForm.java中,门诊结算按钮的事件处理
/** 步骤[6] 门诊结算 */
private void doOutSettle() {
if (!validateCardRead()) return; // 检查读卡有效期
if (txtOutMedfeeSumamt.getText().trim().isEmpty()) {
JOptionPane.showMessageDialog(this,
"请输入医疗费总额!", "提示",
JOptionPane.WARNING_MESSAGE);
return;
}
new Thread(() -> {
try {
String insutype = getInsutypeValue(cmbOutInsutype);
setStatus("正在门诊结算...");
ApiResponseMessage result = apiClient.outpatientSettle(
txtOutMdtrtId.getText().trim(),
txtOutPsnNo.getText().trim(),
insutype,
lastCardResult.getInsuplcAdmdvs(),
lastCardResult.getMdtrtCertNo(),
lastCardResult.getMdtrtCertType(),
txtOutMedfeeSumamt.getText().trim(),
lastCardResult.getPsnCertType(),
lastCardResult.getCertNo()
);
SwingUtilities.invokeLater(() -> {
appendResult(txtOutResult, "2207门诊结算", result);
if (result.isSuccess()) {
// 结算成功后应继续调用4101上传结算清单
// 然后调用P0001打印结算单
}
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() ->
appendException(txtOutResult,
"2207门诊结算", ex));
}
}).start();
}
```
### 9.3 错误回退规则
```
失败发生在步骤[N]时:
→ 回退步骤[N-1]、[N-2]、...、[1]
示例:结算(2207)超时
→ 发起冲正(2207) ← 见第11章冲正实现
→ 如果冲正也失败,记录日志,找医保方处理
```
---
## 10. 住院结算完整流程实现
### 10.1 流程步骤
```
[1] 读卡(1401,Jacob/DLL)
[2] 人员信息(1101,HTTP)
[3] (可选)待遇检查(2001) ← 不可强制绑定!
[4] 入院登记(2401,HTTP)
[5] 上传费用(2402,HTTP) ← 每批≤100条
[6] (可选)信息变更(2403) ← 出院前可变更
[7] 出院办理(2404,HTTP)
[8] 住院结算(2304,HTTP)
[9] 结算清单(4101,HTTP)
[10] 打印单据(P0001,DLL)
```
### 10.2 住院回退规则
```
住院回退必须按结算流程倒序:
结算回退 → 出院回退 → 费用明细撤销 → 入院回退
当发生单边账时:HIS严禁自行删除本地数据!
→ 让经办人找医保局/医保厂商处理
```
**严禁的操作:**
- ❌ 入院登记前先调用住院登记回退
- ❌ 每次费用上传后联动调用住院预结算
- ❌ 门诊费用上传前先删除已结算数据
- ❌ 待遇检查时间传定值并绑定入院登记
---
## 11. 冲正交易实现
### 11.1 可冲正的交易
| 交易编号 | 名称 |
|---------|------|
| 2102 | 药店结算 |
| 2103 | 药店结算撤销 |
| 2207 | 门诊结算 |
| 2208 | 门诊结算撤销 |
| 2304 | 住院结算 |
| 2401 | 入院办理 |
### 11.2 冲正实现
```java
/**
* 冲正交易(对应文档4.8.2)
*
* 关键:在报文头中设置oinfno和omsgid为原交易的信息
* oinfno = 原交易编号
* omsgid = 原发送方报文ID(即原请求的msgid字段)
*/
public ApiResponseMessage reverse(String originalTransNo,
String originalMsgId) {
ApiRequestHeader header = buildHeader(
originalTransNo, new HashMap<>(), null);
// ★ 冲正关键字段
header.setOInfNo(originalTransNo); // 原交易编号
header.setOMsgId(originalMsgId); // 原发送方报文ID
try {
String requestJson =
MAPPER.writeValueAsString(header);
Logger.logRequest(originalTransNo + "(冲正)",
requestJson);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getApiBaseUrl()))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(60))
.POST(HttpRequest.BodyPublishers
.ofString(requestJson))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
String responseJson = response.body();
Logger.logResponse(originalTransNo + "(冲正)",
responseJson);
return MAPPER.readValue(responseJson,
ApiResponseMessage.class);
} catch (Exception e) {
Logger.error("冲正交易[" + originalTransNo + "]失败", e);
return errorResp("冲正失败: " + e.getMessage());
}
}
```
### 11.3 冲正时机
```
结算超时/网络中断/电脑死机
→ 无法获得医保中心结算结果
→ 无法判断医保中心端是否存在本次结算
→ 提示结算失败
→ 立即发起冲正交易
```
---
## 12. 目录下载实现
### 12.1 下载流程
```
循环调用目录下载接口(13xx) → 获取file_qury_no + filename
→ 调用文件下载(9102) → 解析txt文件
→ 取最大ver_name → 下次传入继续下载
→ 直到data_cnt=0
```
### 12.2 版本号规则
| 场景 | ver参数值 |
|------|----------|
| 首次下载(除1305外) | `"0"` |
| 1305首次下载 | `"F002"`(地方目录) |
| 后续增量下载 | 上次最大`ver_name` |
### 12.3 实现代码
```java
/** 目录下载(1301-1319) */
public ApiResponseMessage downloadCatalog(
String catalogTransNo, String verName) {
ObjectNode data = MAPPER.createObjectNode();
data.put("ver", verName);
data.put("admdvs", config.getMdtrtAreaAdmvs());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send(catalogTransNo, input);
}
/** 文件下载(9102) */
public ApiResponseMessage downloadFile(
String fileQueryNo, String filename) {
ObjectNode data = MAPPER.createObjectNode();
data.put("file_qury_no", fileQueryNo);
data.put("filename", filename);
data.put("fixmedins_code", config.getFixMedInsCode());
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("9102", input);
}
```
---
## 13. 费用对账实现
### 13.1 人次计算规则
```
结算人次 = (被冲正后的结算总笔数) - 退费笔数
示例:某机构1月结算200笔,退费20笔,冲正10笔
被冲正后 = 200 - 10 = 190
最终人次 = 190 - 20 = 170
```
### 13.2 实现代码
```java
/** 对总账(5201) */
public ApiResponseMessage reconciliationTotal(
String stmtBegin, String stmtEnd,
String setlOptins, String medfeeSumamt,
String fundPaySumamt, String cashPayamt,
int fixmedinsSetlCnt) {
ObjectNode data = MAPPER.createObjectNode();
data.put("setl_optins", setlOptins);
data.put("stmt_begndate", stmtBegin);
data.put("stmt_enddate", stmtEnd);
data.put("medfee_sumamt", medfeeSumamt);
data.put("fund_pay_sumamt", fundPaySumamt);
data.put("cash_payamt", cashPayamt);
data.put("fixmedins_setl_cnt", fixmedinsSetlCnt);
ObjectNode input = MAPPER.createObjectNode();
input.set("data", data);
return send("5201", input);
}
```
---
## 14. Swing主界面实现
### 14.1 界面结构(9个Tab页)
```
┌─────────────────────────────────────────────┐
│ [系统配置] [读卡] [人员信息] [门诊] [住院] │
│ [冲正] [目录] [对账] [日志] │
├─────────────────────────────────────────────┤
│ │
│ Tab内容区域 │
│ │
├─────────────────────────────────────────────┤
│ 就绪 DLL: 已连接 │
└─────────────────────────────────────────────┘
```
### 14.2 关键UI设计要点
- **业务操作在新线程执行**:避免阻塞UI线程
- **结果通过SwingUtilities.invokeLater回显**:线程安全
- **读卡有效期实时检查**:每次操作前调用`validateCardRead()`
- **日志同步显示**:通过`Logger.setUiLogConsumer()`回调
### 14.3 读卡按钮事件示例
```java
private void doReadCard() {
if (comClient == null || !comClient.isConnected()) {
JOptionPane.showMessageDialog(this,
"请先连接DLL组件!");
return;
}
new Thread(() -> {
try {
setStatus("正在读卡...");
String bt = cmbBusinessType.getSelectedItem()
.toString().split("-")[0].trim();
lastCardResult = comClient.readCard(config, bt);
SwingUtilities.invokeLater(() -> {
txtCardCertNo.setText(
lastCardResult.getCertNo());
txtCardPsnName.setText(
lastCardResult.getPsnName());
txtCardMdtrtCertNo.setText(
lastCardResult.getMdtrtCertNo());
txtCardMdtrtCertType.setText(
lastCardResult.getMdtrtCertType());
lblCardExpire.setText("有效(" +
lastCardResult.getReadTime().format(
DateTimeFormatter.ofPattern("HH:mm:ss"))
+ "起10分钟)");
lblCardExpire.setForeground(Color.GREEN);
setStatus("读卡成功");
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> {
setStatus("读卡失败");
JOptionPane.showMessageDialog(this,
"读卡失败:\n" + ex.getMessage(),
"错误", JOptionPane.ERROR_MESSAGE);
});
}
}).start();
}
```
> 完整Swing界面代码(952行)见源码文件 `MainForm.java`
---
## 15. 程序入口与打包运行
### 15.1 App.java 程序入口
```java
package com.yinhai.medical;
import com.yinhai.medical.ui.MainForm;
import javax.swing.*;
public class App {
public static void main(String[] args) {
// 使用系统外观(Windows风格)
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {}
// 在EDT线程启动界面
SwingUtilities.invokeLater(() -> {
MainForm mainForm = new MainForm();
mainForm.setVisible(true);
});
}
}
```
### 15.2 编译打包
```bash
cd d:\yinhaisoftnew\MedicalInsuranceInterfaceJava
# 编译
mvn clean compile
# 打包为可执行fat-jar
mvn clean package
# 生成文件: target/medical-insurance-interface-1.0.0.jar
```
### 15.3 运行
```bash
# 方式一:直接运行(jacob.dll已在PATH中)
java -jar target/medical-insurance-interface-1.0.0.jar
# 方式二:指定jacob.dll路径
java -Djava.library.path=d:\yinhaisoftnew ^
-jar target/medical-insurance-interface-1.0.0.jar
# 方式三:在IDE中运行
# 直接运行 App.java 的 main 方法
```
### 15.4 运行检查清单
| 检查项 | 说明 |
|--------|------|
| ☐ JDK已安装 | `java -version` 输出11+ |
| ☐ jacob.dll已放到PATH | 否则会报UnsatisfiedLinkError |
| ☐ COM组件已注册 | 已运行regsvr32.bat |
| ☐ yhinterfacenew.ini存在 | 配置文件 |
| ☐ 网络已连通 | 能访问医保接口URL |
---
## 16. 常见问题排查
### 16.1 Jacob相关
| 错误 | 原因 | 解决 |
|------|------|------|
| UnsatisfiedLinkError | jacob.dll未找到 | 放到PATH或指定java.library.path |
| ComFailException | COM组件未注册 | 运行regsvr32.bat |
| 位数不匹配 | 32位JDK用了64位dll | JDK和jacob.dll位数必须一致 |
### 16.2 业务相关
| 错误 | 原因 | 解决 |
|------|------|------|
| 未查询到参保信息 | 险种选错 | 只选主险种310/340/390 |
| 安全认证失败 | 读卡超10分钟 | 重新读卡 |
| 目录数据不全 | 未循环下载 | 循环调用13xx+9102 |
| 字典表数据不全 | 区划传错 | admdvs传450000 |
| 1305下载失败 | ver传了0 | 首次传F002 |
### 16.3 错误前缀规范
| 前缀 | 所属系统 |
|------|---------|
| HIS | HIS系统自身 |
| FSI | 两定接口管理系统 |
| 【广西壮族自治区异地平台】 | 广西异地就医 |
| 【国家平台】 | 国家医保平台 |
| 【XX省/XX市】 | 外省系统 |
---
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)