# 广西医保两定接口 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市】 | 外省系统 |

---

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐