Compare commits

...

53 Commits

Author SHA1 Message Date
1634084aea 支付宝刷脸完善 2025-06-17 15:46:22 +08:00
5a411843b5 支付宝刷脸完善 2025-06-17 15:09:43 +08:00
9ea1026daa 电子医保凭证支付 2025-06-12 11:30:33 +08:00
9c09e8f004 微信刷脸模块 2025-06-09 17:36:17 +08:00
ab05bf79ce 微信刷脸模块 2025-06-09 16:53:36 +08:00
7fc9839038 微信刷脸 2025-05-30 17:39:03 +08:00
9c08c6e7d4 HIS医保 2025-05-29 17:41:06 +08:00
d8d53ebb22 支付宝刷脸-pro文件支付宝配置信息修改为中医院 2025-05-27 11:10:36 +08:00
693947c3a8 支付宝刷脸-pro文件支付宝配置信息修改为中医院 2025-05-27 10:42:36 +08:00
d4e4923bb6 优化小票打印模板,调整参数 2025-05-26 15:49:20 +08:00
1707e0f4ca HIS医保 2025-05-22 18:36:38 +08:00
ad4fb627f9 支付宝刷脸-pro文件支付宝配置信息修改为中医院 2025-05-22 15:12:49 +08:00
fcc0390c80 优化模板,待测试 2025-05-22 14:39:36 +08:00
bdd33043b3 更新部署方式 2025-05-20 18:03:31 +08:00
d9d73434f6 微信刷脸:无授权码 2025-05-07 15:36:05 +08:00
237cf8dbe3 支付宝刷脸模块 2025-05-07 10:06:16 +08:00
d46490fc6b 支付宝刷脸初始化、调用服务 2025-04-29 09:56:43 +08:00
909ad5cc8c 支付宝刷脸初始化、调用服务 2025-04-25 15:49:34 +08:00
chenhongjie
da3aa8d49c fix:pom文件同步启动类名称 2025-04-21 11:25:43 +08:00
0875aca566 微信刷脸SDK POM文件 2025-04-20 09:19:51 +08:00
6dac863d45 微信刷脸SDK 2025-04-20 09:11:11 +08:00
ee0b1ac055 支付宝刷脸初始化方法 2025-04-16 22:01:34 +08:00
67b48d209d dll 2025-04-16 19:19:34 +08:00
71f44a6ec8 dll 2025-04-16 19:11:02 +08:00
5f688ed98a 支付宝刷脸 2025-04-16 17:20:36 +08:00
557852ba40 优化 2025-04-11 19:21:19 +08:00
4cda6728fe 调整社保卡读取参数 2025-04-01 11:06:19 +08:00
02c725de01 feat:增加自动创建路径 2025-03-31 17:40:48 +08:00
8095ead92c Merge remote-tracking branch 'origin/1.0' into 1.0 2025-03-29 12:20:57 +08:00
a5f37fd4f0 fix:修改动态支持A4/A5调换位置 2025-03-29 12:20:50 +08:00
23ca847b93 Merge remote-tracking branch 'origin/1.0' into 1.0 2025-03-28 21:45:50 +08:00
4331fe9919 优化 2025-03-28 21:45:46 +08:00
4376ee65aa Merge remote-tracking branch 'origin/1.0' into 1.0 2025-03-28 13:31:53 +08:00
31734adb7e feat:增加兼容远程pdf下载 2025-03-28 13:31:45 +08:00
4da3a0670d 配置更新 2025-03-26 21:25:05 +08:00
b018f819d1 优化 2025-03-26 15:09:06 +08:00
eb038419a1 Merge remote-tracking branch 'origin/1.0' into 1.0 2025-03-26 14:44:49 +08:00
78a3bceecd 优化 2025-03-26 14:44:41 +08:00
58c1975b05 Merge remote-tracking branch 'origin/1.0' into 1.0
# Conflicts:
#	pom.xml
2025-03-26 14:39:54 +08:00
906928ea0f 更新配置 2025-03-26 14:37:56 +08:00
3eb441d41c 优化 2025-03-26 14:01:23 +08:00
12f2374ccf 优化 2025-03-26 13:33:30 +08:00
aa0dc12805 优化 2025-03-26 13:23:10 +08:00
6755206d2e Merge remote-tracking branch 'origin/1.0' into 1.0
# Conflicts:
#	src/main/resources/templates/department.html
2025-03-26 13:22:25 +08:00
3938301b66 门诊小票T2模板添加以class命名的文件,还原为内联样式 2025-03-26 13:21:55 +08:00
546f644ffd fix:pom文件中,移除对resource的排除 2025-03-26 13:11:03 +08:00
ffaa06f934 fix:恢复原始样式 2025-03-26 13:10:41 +08:00
20b25042b5 fix:修复图片分割出现黑边问题 2025-03-26 13:09:25 +08:00
503ff3a73e fix:修改异常提示 2025-03-26 13:01:53 +08:00
ee2115e367 门诊小票T2模板优化 2025-03-26 11:12:16 +08:00
c9592dfd90 feat:增加终端号配置 2025-03-26 11:08:05 +08:00
85b2b1bd87 Merge remote-tracking branch 'origin/1.0' into 1.0
# Conflicts:
#	src/main/resources/templates/department.html
2025-03-26 10:54:16 +08:00
6c126a6757 feat:新增门诊缴费-T2模板为完全动态生成 2025-03-26 10:53:04 +08:00
71 changed files with 4996 additions and 455 deletions

6
doc/lib/command.bat Normal file
View File

@@ -0,0 +1,6 @@
echo off
:: 添加jar包到本地仓库
cmd /k "mvn install:install-file -Dfile=jacob-1.21.jar -DgroupId=com.jacob -DartifactId=jacob -Dversion=1.21 -Dpackaging=jar"
pause

BIN
doc/lib/jacob-1.21-x64.dll Normal file

Binary file not shown.

BIN
doc/lib/jacob-1.21-x86.dll Normal file

Binary file not shown.

BIN
doc/lib/jacob-1.21.jar Normal file

Binary file not shown.

View File

@@ -0,0 +1,23 @@
@echo off
:: 医保程序地址
set CHSPATH=D:/Project/CHS
:: jar名称
set NAME=yinyitong-dll-stand
:: 端口号
set PROT=5946
echo 关闭端口进程:%PROT%
for /f "tokens=1-5" %%i in ('netstat -ano^|findstr ":%PROT%"') do taskkill /pid %%m -t -f
echo 启动:%NAME%
cd %CHSPATH%
start javaw.exe -Dfile.encoding=UTF-8 -Djava.library.path=%CHSPATH% -Dlog.path=%~dp0 -jar %~dp0\%NAME%.jar --server.port=%PROT%
echo 启动完成
exit
::pause

View File

@@ -1,8 +1,25 @@
<service>
<id>yinyitong-dll-stand</id>
<name>yinyitong-dll-stand</name>
<description>银医通-台式机-DLL调用服务</description>
<!-- 环境变量 文件名称 -->
<env name="NAME" value="yinyitong-dll-stand"/>
<!-- 服务ID -->
<id>dpkj-%NAME%</id>
<!-- 服务名称 -->
<name>%BASE%</name>
<!-- 服务描述 -->
<description>银医台式机中间服务调用程序</description>
<!-- 启动命令 -->
<executable>java</executable>
<arguments>-jar %BASE%\yinyitong-dll-stand.jar</arguments>
<!-- 启动参数 -->
<arguments>-jar %BASE%\%NAME%.jar</arguments>
<!-- 日志-->
<logpath>%BASE%\logs\winws</logpath>
<!-- 按大小和时间滚动模式 -->
<log mode="roll-by-size-time">
<sizeThreshold>10240</sizeThreshold>
<pattern>yyyyMMdd</pattern>
<autoRollAtTime>00:00:00</autoRollAtTime>
</log>
</service>

67
pom.xml
View File

@@ -19,8 +19,8 @@
<properties>
<java.version>1.8</java.version>
<hutool.version>5.8.25</hutool.version>
<jna.version>5.14.0</jna.version>
<hutool.version>5.8.36</hutool.version>
<jna.version>5.17.0</jna.version>
<pdfbox.version>3.0.2</pdfbox.version>
</properties>
@@ -36,6 +36,12 @@
<optional>true</optional>
</dependency>
<!-- 数据校验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -50,21 +56,38 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- 调用DLL -->
<!-- <dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>-->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<artifactId>jna-jpms</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<!--<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>-->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform-jpms</artifactId>
<version>${jna.version}</version>
</dependency>
<!-- 调用DLL - COM库 -->
<dependency>
<groupId>com.jacob</groupId>
<artifactId>jacob</artifactId>
<version>1.21</version>
</dependency>
<!--PDF转换工具-->
@@ -119,17 +142,23 @@
<version>3.1.28</version> <!-- 可以根据实际情况调整版本 -->
</dependency>
<!--微信支付-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.15</version>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.7.0</version>
</dependency>
<!-- 数据校验-->
<!--添加JDOM库-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
</dependencies>
@@ -183,7 +212,7 @@
<!--jar包名字是否包含唯一版本标识-->
<useUniqueVersions>false</useUniqueVersions>
<!--指定含main方法的主类入口-->
<mainClass>com.dpkj.Application</mainClass>
<mainClass>com.dpkj.StandDllApplication</mainClass>
</manifest>
<manifestEntries>
<!--MANIFEST.MF 中 Class-Path 加入资源文件目录用命令java -jar时就不用-Dloader.path指定外部资源路径了 -->
@@ -192,11 +221,11 @@
</archive>
<!-- 打包时从jar包里排除资源文件 -->
<excludes>
<exclude>*.yml</exclude>
<!--<exclude>*.yml</exclude>
<exclude>*.xml</exclude>
<exclude>templates/**</exclude>
<exclude>win32-x86-64/**</exclude>
<exclude>win32-x86/**</exclude>
<exclude>win32-x86/**</exclude>-->
</excludes>
<!-- 指定项目打成jar包输出位置 -->
<outputDirectory>${project.build.directory}/output</outputDirectory>
@@ -220,7 +249,7 @@
</executions>
</plugin>
<!--拷贝资源文件! 插件maven-jar-plugin只负责打包时排除文件 而把资源文件拷贝到外部resource目录就需要maven-dependency-plugin插件-->
<plugin>
<!--<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
@@ -232,16 +261,16 @@
<configuration>
<resources>
<resource>
<!--拷贝此目录下的所有文件到指定的外部目录。只负责拷贝而不是从jar包中排除-->
&lt;!&ndash;拷贝此目录下的所有文件到指定的外部目录。只负责拷贝而不是从jar包中排除&ndash;&gt;
<directory>src/main/resources</directory>
</resource>
</resources>
<!-- 把“<resource><directory>”指定目录中的文件输出到此处指定目录 -->
&lt;!&ndash; 把“<resource><directory>”指定目录中的文件输出到此处指定目录 &ndash;&gt;
<outputDirectory>${project.build.directory}/output/resources</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugin>-->
</plugins>
</build>

View File

@@ -12,10 +12,10 @@ import java.net.UnknownHostException;
@Slf4j
@SpringBootApplication
public class Application {
public class StandDllApplication {
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(Application.class, args);
ConfigurableApplicationContext application = SpringApplication.run(StandDllApplication.class, args);
Environment env = application.getEnvironment();
String ip = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");

View File

@@ -0,0 +1,22 @@
package com.dpkj.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Auther: 萧道子
* @Date: 2024/4/28 14:55
* @Description:
*/
@Data
@Component
@ConfigurationProperties(prefix = "dpkj.his")
public class HisConfig {
/**
* 调用者ID
*/
private String operationId;
}

View File

@@ -2,7 +2,6 @@ package com.dpkj.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
@@ -29,4 +28,20 @@ public class PrinterConfig {
* 波特率 串口连接下使用
*/
private Integer baudRate;
/**
* 终端号
*/
private String terminalNumber;
/**
* 时间格式
*/
private String timeType;
/**
* 只有两层要么是A4要么是A5
*/
private String levelOne;
}

View File

@@ -0,0 +1,42 @@
package com.dpkj.common.constant;
/**
* @description:
* @author: Zhangxue
* @time: 2025/5/28 11:04
*/
public interface WxConstant {
/**
* 门店编号, 由商户定义, 各门店唯一。
* TODO 修改
*/
String STORE_ID = "1111111111111";
//门店名称,由商户定义。(可用于展示);中文会导致调用失败
String STORE_TEXT = "device8998No1";
//终端设备编号,由商户定义
String DEVICE_ID = "device8998";
//版本号。固定为1
String VERSION = "1";
//参数签名,使用MD5
String SING_TYPE = "MD5";
/**
* 支付类型
*/
String FACEPAY = "FACEPAY";
/**
* 结果状态 成功
*/
String STATE_SUCCESS = "SUCCESS";
/**
* 结果状态 失败
*/
String STATE_FAIL = "FAIL";
}

View File

@@ -5,8 +5,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
@@ -15,13 +13,12 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* controller控制器异常处理接口实现类
* <p>其它module可以集成此类进行controller层的异常处理</p>
*
* @author <a href="https://gitee.com/shi-chongli">石头人</a>
*/
@Slf4j
@@ -38,41 +35,37 @@ public class ControllerAdvice {
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<String> bindingException(MethodArgumentNotValidException e) {
// 获得所有校验出错的返回集
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
// 循环获得所有校验异常的字段
Map<String, String> fieldMap = new HashMap<>();
for (FieldError fieldError : fieldErrors) {
fieldMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
String errMsg = fieldMap.values().toString().replaceAll("]", "").replaceAll("\\[", "");
log.warn(errMsg);
// 返回给前端
String errMsg = e.getBindingResult()
.getFieldErrors()
.parallelStream()
.map(m -> /*m.getField() + ":" +*/ m.getDefaultMessage())
.collect(Collectors.joining(""));
log.error(e.getMessage(), e);
return Result.error(errMsg);
}
/**
* 处理空指针异常
*
* @param nullPointerException 空指针异常
* @return Result
*/
@ExceptionHandler(value = NullPointerException.class)
public Result<String> nullPointException(NullPointerException nullPointerException) {
log.error("空指针异常类型: {},信息: {}", nullPointerException.getClass(),nullPointerException.getMessage());
log.error("空指针异常类型: {},信息: {}", nullPointerException.getClass(), nullPointerException);
return Result.error(ErrorEnum.NULL_POINTER_EXCEPTION);
}
/**
* 所有的运行时异常,抛出异常
*
* @param throwable 异常
* @return Result
*/
@ExceptionHandler(value = Throwable.class)
public Result<String> handleException(Throwable throwable) {
log.error("异常类型: {}, {}, 信息为: {}", throwable.getCause(), throwable.getClass(), throwable.getMessage());
if (throwable instanceof RRException){
if (throwable instanceof RRException) {
RRException rrException = (RRException) throwable;
return Result.error(rrException.getCode(), rrException.getMsg());
}
@@ -81,24 +74,26 @@ public class ControllerAdvice {
/**
* http信息无可读
*
* @param e 异常
* @return Result
*/
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public Result<String> httpMessageNotReadAbleException(HttpMessageNotReadableException e){
log.warn("异常类型: {} 无可读信息: {}", e.getClass(), e.getMessage());
public Result<String> httpMessageNotReadAbleException(HttpMessageNotReadableException e) {
log.warn("异常类型: {} 无可读信息: ", e.getClass(), e);
return Result.error(ErrorEnum.HTTP_MESSAGE_NOT_READABLE_EXCEPTION);
}
/**
* 运行时异常
*
* @param e 运行异常对象
* @return Result
*/
@ExceptionHandler(value = RuntimeException.class)
public Result<String> runtimeException(RuntimeException e){
log.error("运行时异常:{}", e.getMessage());
if (e instanceof RRException){
public Result<String> runtimeException(RuntimeException e) {
log.error("运行时异常:", e);
if (e instanceof RRException) {
RRException rrException = (RRException) e;
return Result.error(rrException.getCode(), rrException.getMsg());
}
@@ -107,32 +102,35 @@ public class ControllerAdvice {
/**
* 请求不支持
*
* @return Result
*/
@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
public Result<String> httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e){
log.warn("暂不支持该请求: {}", e.getMessage());
public Result<String> httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.warn("暂不支持该请求: ", e);
return Result.error("暂不支持此请求方式");
}
/**
* 参数类型错误
*
* @return Result
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public Result<String> methodArgument(MethodArgumentTypeMismatchException e){
log.warn("参数发生错误: {}", e.getMessage());
public Result<String> methodArgument(MethodArgumentTypeMismatchException e) {
log.warn("参数发生错误: ", e);
return Result.error("参数发生错误");
}
/**
* 缺少请求参数
*
* @param e 缺少请求参数异常
* @return Result
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result<String> exception(MissingServletRequestParameterException e){
log.warn("缺少请求参数: {}", e.getMessage());
public Result<String> exception(MissingServletRequestParameterException e) {
log.warn("缺少请求参数: ", e);
return Result.error("缺少请求参数");
}

View File

@@ -0,0 +1,49 @@
package com.dpkj.common.utils;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
/**
* @Auther: 萧道子
* @Date: 2025/3/19 20:01
* @Description:
*/
public class IDGenerator {
private static Snowflake snowflake = IdUtil.getSnowflake(1, 1);
private static Snowflake snowflake2 = IdUtil.getSnowflake(1, 2);
/**
* 取雪花算法ID
*
* @return java.lang.String
* @author 萧道子 2025/3/19
*/
public static String getSnowflakeIdToStr() {
return snowflake.nextIdStr();
}
/**
* 取雪花算法ID
*
* @return java.lang.String
* @author 萧道子 2025/3/19
*/
public static Long getSnowflakeIdToLong() {
return snowflake.nextId();
}
/**
* 获取 Traceid
*
* @return java.lang.String
* @author 萧道子 2025/4/1
*/
public static String getTraceid() {
return snowflake2.nextIdStr();
}
}

View File

@@ -77,7 +77,7 @@ public class TemplateUtils {
resolver.setTemplateMode("HTML");
// 设置模板引擎使用这个自定义的模板解析器
templateEngine.setTemplateResolver(resolver);
}else {
} else {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
@@ -111,9 +111,10 @@ public class TemplateUtils {
/**
* 生成图片
*
* @param html html内容
*/
private BufferedImage generate(String html, int width, int height){
private BufferedImage generate(String html, int width, int height) {
try {
// 转换为xhtml
String xhtml = this.htmlToXhtml(html);
@@ -132,21 +133,22 @@ public class TemplateUtils {
/**
* 通过JsonObject进行context内容填充
*
* @param data jsonObject
* @return context
*/
private Context getContext(JSONObject data) {
// 创建Thymeleaf上下文
Context context = new Context();
if ( data != null) {
if (data != null) {
Set<String> keys = data.keySet();
for (String key : keys) {
// 判单是否有图片生成统一后面采用的是_2base64Type
String[] split = key.split("_");
if (split.length > 1 && split[1].equals("2base64Type")) {
int type = split.length > 2 ? Integer.parseInt(split[2]) : 1;
int width = split.length > 2 ? Integer.parseInt(split[3]) : 100;
int height = split.length > 3 ? Integer.parseInt(split[4]) : 100;
int width = split.length > 2 ? (Integer.parseInt(split[3]) + 50) : 150;// 台式的二维码宽高需要比壁挂的多加50
int height = split.length > 3 ? (Integer.parseInt(split[4]) + 50) : 150;// 台式的二维码宽高需要比壁挂的多加50
// 如果是图片类型,需要进行base64转换
String base64 = this.generateQRCode(type, String.valueOf(data.get(key)), width, height);
context.setVariable(split[0], "data:image/jpeg;base64," + base64);
@@ -168,8 +170,9 @@ public class TemplateUtils {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 设置字符编码为 UTF-8
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); // 设置纠错级别
// 设置外边距为 0 以去除白边
hints.put(EncodeHintType.MARGIN, 0);
// 设置外边距为1
hints.put(EncodeHintType.MARGIN, 1);
if (type == 1) {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
@@ -182,7 +185,7 @@ public class TemplateUtils {
qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
}
if ( qrImage == null ){
if (qrImage == null) {
throw new RRException("图片渲染失败");
}
@@ -212,6 +215,7 @@ public class TemplateUtils {
/**
* xhtml 转换为 Document
*
* @param xhtml xhtml
* @return document
* @throws Exception e
@@ -232,6 +236,7 @@ public class TemplateUtils {
/**
* 转换将html转换为xhtml
*
* @param html html内容
* @return xhtml
*/
@@ -259,9 +264,10 @@ public class TemplateUtils {
/**
* 通过document转换为图片
*
* @param document doc
* @param width 图片的宽度
* @param height 图片的高度
* @param width 图片的宽度
* @param height 图片的高度
* @return bufferedImage
*/
private BufferedImage createImageToDocument(Document document, int width, int height) {
@@ -309,15 +315,16 @@ public class TemplateUtils {
/**
* 校验这个模板内容是不是html字符串而非模板名称
*
* @param template template
* @return 是否是html字符串
*/
private boolean checkIsHtml(String template){
private boolean checkIsHtml(String template) {
try {
String pattern = "<(\"[^\"]*\"|'[^']*'|[^'\">])*>";
Pattern r = Pattern.compile(pattern);
return r.matcher(template).find();
}catch (Exception e) {
} catch (Exception e) {
return false;
}
}
@@ -325,6 +332,7 @@ public class TemplateUtils {
/**
* 将document对象转换为字符串
*
* @param doc document
* @return document转换为的字符串
*/

View File

@@ -0,0 +1,45 @@
package com.dpkj.common.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 接口返回数据格式
*/
@Data
@Accessors(chain = true)
public class ResultData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 请求时间
*/
private String requestTime;
/**
* 请求内容
*/
private String requestContent;
/**
* 响应时间
*/
private String responseTime;
/**
* 响应内容
*/
private String responseContent;
/**
* 患者ID
*/
private String patientId;
/**
* 处理后的响应内容
*/
private Object result;
}

View File

@@ -1,19 +1,15 @@
package com.dpkj.modules.chs.controller;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.config.ChsConfig;
import com.dpkj.common.constant.ChsConst;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.chs.dll.AlipayDll;
import com.dpkj.modules.chs.entity.AlipayEcRequestData;
import com.dpkj.modules.chs.service.IAlipayService;
import com.dpkj.common.vo.ResultData;
import com.dpkj.modules.chs.service.IHispayService;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.dpkj.modules.chs.vo.OutpatientBeginModel;
import com.dpkj.modules.chs.vo.OutpatientFinalModel;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -30,21 +26,92 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/chs/hispay")
public class HispayController {
private final IHispayService hispayService;
private final ChsConfig charsConfig;
private final IHispayService iHispayService;
@GetMapping("test")
public Result<?> test() {
/**
* 通过医保卡或医保电子凭证读卡
*
* @return com.dpkj.common.vo.Result<?>
* @author 萧道子 2025/5/20
*/
@PostMapping("findReadCode")
public Result<?> findReadCode() {
try {
return Result.ok("成功", null);
JSONObject res = iHispayService.readCode();
return Result.ok("成功", res);
} catch (Exception e) {
e.printStackTrace();
return Result.error("失败");
log.error("[HispayController][getPatientInfo][按医保电子凭证读卡] ERR{}", e.getMessage());
return Result.error(e.getMessage());
}
}
/**
* 通过医保卡获取患者信息
*
* @param data :
* @return com.dpkj.common.vo.Result<?>
* @author 萧道子 2025/5/28
*/
@PostMapping("findReadCard")
public Result<?> findReadCard(@RequestBody JSONObject data) {
try {
String password = data.getString("password");
if (StrUtil.isEmpty(password)) {
throw new RuntimeException("密码不可为空");
}
JSONObject res = iHispayService.readCard(password);
return Result.ok("成功", res);
} catch (Exception e) {
e.printStackTrace();
log.error("[HispayController][readCardByCard][按医保卡读卡] ERR{}", e.getMessage());
return Result.error(e.getMessage());
}
}
/**
* 门诊缴费-预算
*
* @param data :
* @return com.dpkj.common.vo.Result<?>
* @author 萧道子 2025/5/27
*/
@PostMapping("chsCodeAsOutpatientBegin")
public Result<?> chsCodeAsOutpatientBegin(@RequestBody @Validated OutpatientBeginModel data) {
try {
log.info("[HispayController][chsCodeAsOutpatientBegin][门诊缴费-预算-电子医保凭证] 参数:{}", data);
ResultData res = iHispayService.chsCodeAsOutpatientBegin(data);
return Result.ok("成功", res);
} catch (Exception e) {
e.printStackTrace();
log.info("[HispayController][chsCodeAsOutpatientBegin][门诊缴费-预算-电子医保凭证] 失败:{}", e.getMessage());
return Result.error(e.getMessage());
}
}
/**
* 门诊缴费-结算
*
* @param data :
* @return com.dpkj.common.vo.Result<?>
* @author 萧道子 2025/5/27
*/
@PostMapping("chsCodeAsOutpatientFinal")
public Result<?> chsCodeAsOutpatientFinal(@RequestBody @Validated OutpatientFinalModel data) {
try {
log.info("[HispayController][chsCodeAsOutpatientFinal][门诊缴费-结算-电子医保凭证] 参数:{}", data);
ResultData res = iHispayService.chsCodeAsOutpatientFinal(data);
return Result.ok("成功", res);
} catch (Exception e) {
e.printStackTrace();
log.info("[HispayController][chsCodeAsOutpatientFinal][门诊缴费-结算-电子医保凭证] 失败:{}", e.getMessage());
return Result.error(e.getMessage());
}
}
}

View File

@@ -21,7 +21,7 @@ public class AlipayDll {
*/
public static Dll instance() throws DllRegistrationException {
try {
return Native.load("NationECCode", Dll.class);
return Native.load("AlipayChs", Dll.class);
} catch (UnsatisfiedLinkError e) {
log.info("[AlipayDll][getPrintSDK][医保动态库] SDK注册失败{}", e.getMessage());
throw new DllRegistrationException("Failed to load AlipayDll library: ", e);

View File

@@ -1,5 +1,6 @@
package com.dpkj.modules.chs.dll;
import com.jacob.activeX.ActiveXComponent;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
@@ -13,47 +14,5 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HispayDll {
/**
* 获取 AlipayDll 实例,同时注册 AlipayDll 控件。
*
* @return AlipayDll 实例
* @throws DllRegistrationException 如果注册控件失败,抛出此异常
*/
public static Dll instance() throws DllRegistrationException {
try {
return Native.load("PayClient", Dll.class);
} catch (UnsatisfiedLinkError e) {
log.info("[HispayDll][getPrintSDK][医保动态库] SDK注册失败{}", e.getMessage());
throw new DllRegistrationException("Failed to load AlipayDll library: ", e);
}
}
/**
* 定义自定义异常类,用于表示注册控件时发生的错误
*/
public static class DllRegistrationException extends Exception {
public DllRegistrationException(String message) {
super(message);
}
public DllRegistrationException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 定义接口映射本地库中的函数。
*/
public interface Dll extends Library {
/**
* 设置打印端口和波特率。
*
* @return 返回操作结果代码
*/
String NationEcTrans(String strUrl, String InData, Pointer OutData);
}
}

View File

@@ -1,4 +1,47 @@
package com.dpkj.modules.chs.service;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.vo.ResultData;
import com.dpkj.modules.chs.vo.OutpatientBeginModel;
import com.dpkj.modules.chs.vo.OutpatientFinalModel;
public interface IHispayService {
/**
* 通过医保电子凭证读卡
*
* @return com.alibaba.fastjson.JSONObject
* @author 萧道子 2025/5/28
*/
JSONObject readCode();
/**
* 通过医保卡-读卡
*
* @return com.alibaba.fastjson.JSONObject
* @author 萧道子 2025/5/28
*/
JSONObject readCard(String password);
/**
* 门诊缴费-预算 电子凭证支付
*
* @param data :
* @return com.dpkj.common.vo.Result<?>
* @author 萧道子 2025/5/27
*/
ResultData chsCodeAsOutpatientBegin(OutpatientBeginModel data);
/**
* 门诊缴费-结算 电子凭证支付
*
* @param data :
* @return com.dpkj.common.vo.Result<?>
* @author 萧道子 2025/5/27
*/
ResultData chsCodeAsOutpatientFinal(OutpatientFinalModel data);
}

View File

@@ -1,12 +1,28 @@
package com.dpkj.modules.chs.service.impl;
import cn.hutool.core.lang.Console;
import com.dpkj.modules.chs.dll.HispayDll;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.config.ChsConfig;
import com.dpkj.common.config.HisConfig;
import com.dpkj.common.utils.IDGenerator;
import com.dpkj.common.vo.ResultData;
import com.dpkj.modules.chs.service.IHispayService;
import com.dpkj.modules.chs.vo.OutpatientBeginModel;
import com.dpkj.modules.chs.vo.OutpatientFinalModel;
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.ComThread;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import java.util.Map;
import javax.annotation.PostConstruct;
/**
* @Auther: 萧道子
@@ -15,23 +31,307 @@ import javax.annotation.PostConstruct;
*/
@Slf4j
@Service
@AllArgsConstructor
public class HispayServiceImpl implements IHispayService {
private HispayDll.Dll dll = HispayDll.instance();
private final HisConfig hisConfig;
private final ChsConfig chsConfig;
public HispayServiceImpl() throws HispayDll.DllRegistrationException {
}
@PostConstruct
public void postConstruct() {
log.info("[HispayServiceImpl][postConstruct][医保DLL-HIS] 初始化动态链接库");
initPrinter();
/**
* 获取HIS医保实例
*
* @return com.jacob.activeX.ActiveXComponent
* @author 萧道子 2025/5/21
*/
private ActiveXComponent instanceActive() {
try {
// 初始化
ComThread.InitSTA();
// 获取COM对象
ActiveXComponent active = new ActiveXComponent("PayClient.clsPayClient");
log.info("[HispayServiceImpl][instanceActive][HIS医保COM库] 加载成功");
return active;
} catch (UnsatisfiedLinkError e) {
log.info("[HispayServiceImpl][instanceActive][HIS医保COM库] 加载失败:{}", e.getMessage());
throw new RuntimeException("HIS医保COM库加载失败" + e.getMessage(), e);
}
}
private void initPrinter() {
Console.log(dll);
/**
* 释放资源
*
* @return void
* @author 萧道子 2025/5/21
*/
private void releaseActive() {
ComThread.Release();
}
/**
* 处理参数
*
* @param params : 请求参数
* @param password : 密码
* @return java.lang.String
* @author 萧道子 2025/5/27
*/
private String processParameters(JSONObject params, String password) {
JSONObject req = new JSONObject() {{
put("timestamp", DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss")); // 请求发送时间
put("requestid", IDGenerator.getSnowflakeIdToStr()); // 唯一请求id
put("operid", hisConfig.getOperationId()); // 调用者代码
put("password", password); // 密码
put("params", params);
}};
Document document = XmlUtil.mapToXml(req, "request");
return XmlUtil.toStr(document, "UTF-8", false, true);
}
/**
* 校验结果
*
* @param resStr :
* @return com.alibaba.fastjson.JSONObject
* @author 萧道子 2025/5/28
*/
private JSONObject verifyResult(String resStr) {
if (StrUtil.isBlank(resStr)) {
throw new RuntimeException("信息获取失败");
}
Map<String, Object> resMap = XmlUtil.xmlToMap(resStr);
JSONObject resJson = new JSONObject(resMap);
if (StrUtil.equals(resJson.getString("resultCode"), "1")) {
throw new RuntimeException(resJson.getString("resultMessage"));
}
if (!resJson.containsKey("result")) {
throw new RuntimeException("result数据为空");
}
return resJson.getJSONObject("result");
}
@Override
public JSONObject readCode() {
// 加载资源
Dispatch dispatch = instanceActive();
/** 1、组装参数 */
JSONObject val = new JSONObject()
.fluentPut("cardtype", "9") // 1.就诊卡2.医保卡5.门诊号6.患者姓名和电话号码8.电子健康码/卡9.医保电子凭证
.fluentPut("cardno", "") // 患者就诊卡号
.fluentPut("sfzh", "") // 身份证号
.fluentPut("hzxm", "") // 患者姓名
.fluentPut("phone", ""); // 患者电话号码
String params = processParameters(val, null);
log.info("[HispayServiceImpl][readCode][医保读卡-电子凭证] 接口入参:{}", params);
/** 2、调用COM函数 */
Variant vres = new Variant("", true);
Variant call = Dispatch.call(dispatch, "fRun", "BMZXX010", params, vres);
String resStr = vres.getStringRef();
log.info("[HispayServiceImpl][readCode][医保读卡-电子凭证] call返回值{} 结果:{}", call, resStr);
// 释放资源
releaseActive();
/** 3、处理读卡结果 */
JSONObject result = verifyResult(resStr);
if (!result.containsKey("item")) {
throw new RuntimeException("item数据为空");
}
return result.getJSONObject("item");
}
@Override
public JSONObject readCard(String password) {
// 加载资源
Dispatch dispatch = instanceActive();
/** 1、组装参数 */
JSONObject val = new JSONObject()
.fluentPut("cardtype", "2") // 1.就诊卡2.医保卡5.门诊号6.患者姓名和电话号码8.电子健康码/卡9.医保电子凭证
.fluentPut("cardno", "") // 患者就诊卡号
.fluentPut("sfzh", "") // 身份证号
.fluentPut("hzxm", "") // 患者姓名
.fluentPut("phone", ""); // 患者电话号码
String params = processParameters(val, password);
log.info("[HispayServiceImpl][readCard][医保读卡-医保卡] 接口入参:{}", params);
/** 2、调用COM函数 */
Variant vres = new Variant("", true);
Variant call = Dispatch.call(dispatch, "fRun", "BMZXX010", params, vres);
String resStr = vres.getStringRef();
log.info("[HispayServiceImpl][readCard][医保读卡-医保卡] call返回值{} 结果:{}", call, resStr);
// 释放资源
releaseActive();
/** 3、处理读卡结果 */
JSONObject result = verifyResult(resStr);
if (!result.containsKey("item")) {
throw new RuntimeException("item数据为空");
}
return result.getJSONObject("item");
}
// public JSONObject getPatientInfo_old(String type, String content) {
// Dispatch dispatch = instanceActive();
//
// /**
// * 1、删除保存HIS读卡内容的文件 避免读取到错误信息
// */
// // 获取HIS-CHS医保库路径
// String chsPath = System.getProperty("java.library.path");
// String filePath = chsPath + "/" + chsConfig.getFileName();
// // 删除文件
// FileUtil.del(FileUtil.file(filePath));
//
// /**
// * 2、组装参数
// */
// JSONObject val = new JSONObject()
// .fluentPut("cardtype", type) // 1.就诊卡2.医保卡5.门诊号6.患者姓名和电话号码8.电子健康码/卡9.医保电子凭证
// .fluentPut("cardno", "") // 患者就诊卡号
// .fluentPut("sfzh", "") // 身份证号
// .fluentPut("hzxm", "") // 患者姓名
// .fluentPut("phone", ""); // 患者电话号码
// String params = processParameters(val, content);
// log.info("[HispayServiceImpl][getPatientInfo][医保读卡] 接口入参:{}", params);
//
// /**
// * 3、调用COM函数
// */
// Variant rest = new Variant();
// Variant call = Dispatch.call(dispatch, "fRun", "BMZXX010", params, rest);
// log.info("[HispayServiceImpl][getPatientInfo][医保读卡] call返回值{}", call);
//
// // 释放资源
// releaseActive();
//
// /**
// * 4、处理读卡结果
// */
// // COM函数调用之后会生成新的文件 需要判断
// File file = FileUtil.file(filePath);
// if (!file.exists()) {
// throw new RuntimeException("读卡失败:数据未读取");
// }
//
// // 获取读卡结果
// FileReader fileReader = new FileReader(file, "GBK");
// String data = fileReader.readString();
//
// JSONObject result;
// try {
// result = JSONObject.parseObject(data);
// log.info("[HispayServiceImpl][getPatientInfo][医保读卡] 读卡值:{}", result);
// } catch (Exception e) {
// throw new RuntimeException("读卡失败:" + data);
// }
// return result;
// }
private ResultData outpatientBudget(OutpatientBeginModel data) {
// 加载资源
ActiveXComponent dispatch = instanceActive();
/** 1、组装参数 */
JSONObject val = new JSONObject()
.fluentPut("patid", data.getPatientId()) // 病人ID
.fluentPut("cfxhhj", data.getPrescriptionNo()) // 划价单据,格式:单据1,单据2,单据3
.fluentPut("czksfbz", "0") // 是否扣院内账户0不使用院内账户1使用院内账户默认不使用院内账户
.fluentPut("zfjsbz", "0") // 是否自费结算0根据医保代码缴费1自费结算默认自费结算
.fluentPut("ybrc", ""); // 医保入参xml节点
String params = processParameters(val, data.getPassword());
log.info("[HispayServiceImpl][outpatientBudget][门诊缴费-预算] 接口入参:{}", params);
String requestTime = DateUtil.now();
/** 2、调用COM函数 */
Variant resVariant = new Variant("", true);
Variant call = Dispatch.call(dispatch, "fRun", "BMZJF001", params, resVariant);
String responseTime = DateUtil.now();
String resStr = resVariant.getStringRef();
log.info("[HispayServiceImpl][outpatientBudget][门诊缴费-预算] call返回值{} 结果:{}", call, resStr);
// 释放资源
releaseActive();
/** 3、处理结果 */
JSONObject result = verifyResult(resStr);
return new ResultData()
.setRequestTime(requestTime)
.setRequestContent(params)
.setResponseTime(responseTime)
.setPatientId(data.getPatientId())
.setResponseContent(resStr)
.setResult(result);
}
@Override
public ResultData chsCodeAsOutpatientBegin(OutpatientBeginModel data) {
// 用户读卡-生成token
JSONObject userInfo = readCode();
String patientId = userInfo.getString("patid");
data.setPatientId(patientId);
return outpatientBudget(data);
}
@Override
public ResultData chsCodeAsOutpatientFinal(OutpatientFinalModel data) {
// 加载资源
ActiveXComponent dispatch = instanceActive();
/** 1、组装参数 */
JSONObject val = (JSONObject) JSON.toJSON(data);
String params = processParameters(val, null);
log.info("[HispayServiceImpl][chsCodeAsOutpatientFinal][门诊缴费-结算] 接口入参:{}", params);
String requestTime = DateUtil.now();
/** 2、调用COM函数 */
Variant resVariant = new Variant("", true);
Variant call = Dispatch.call(dispatch, "fRun", "BMZJF002", params, resVariant);
String responseTime = DateUtil.now();
String resStr = resVariant.getStringRef();
log.info("[HispayServiceImpl][chsCodeAsOutpatientFinal][门诊缴费-结算] call返回值{} 结果:{}", call, resStr);
// 释放资源
releaseActive();
// TODO BEGIN 萧道子 2025/6/10 : 模拟参数
// String resStr = "<response><resultCode>0</resultCode><resultMessage/><result><sjh>123456789</sjh><sfrq>2025-06-10 10:23:14</sfrq><bz/><pyckjh/><fyckjh/><yflshjh/><ybcc/></result></response>";
// TODO END 萧道子 2025/6/10 : 模拟参数
/** 3、处理结果 */
JSONObject result = verifyResult(resStr);
return new ResultData()
.setRequestTime(requestTime)
.setRequestContent(params)
.setResponseTime(responseTime)
.setPatientId(data.getPatid())
.setResponseContent(resStr)
.setResult(result);
}

View File

@@ -0,0 +1,37 @@
package com.dpkj.modules.chs.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
/**
* @Auther: 萧道子
* @Date: 2025/5/27 16:52
* @Description:
*/
@Data
@Accessors(chain = true)
public class OutpatientBeginModel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 病人ID
*/
// @NotEmpty(message = "患者ID不可为空!")
private String patientId;
/**
* 处方单号 - 多个逗号拼接
*/
@NotEmpty(message = "处方单号不可为空!")
private String prescriptionNo;
/**
* 病人ID
*/
// @NotEmpty(message = "密码不可为空!")
private String password;
}

View File

@@ -0,0 +1,47 @@
package com.dpkj.modules.chs.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
/**
* @Auther: 萧道子
* @Date: 2025/5/27 16:52
* @Description: 结算
*/
@Data
@Accessors(chain = true)
public class OutpatientFinalModel implements Serializable {
private static final long serialVersionUID = 1L;
@NotEmpty(message = "患者ID不可为空!")
private String patid;
@NotEmpty(message = "收据号不可为空!")
private String sjh;
@NotEmpty(message = "总金额不可为空!")
private String zje;
@NotEmpty(message = "应付金额不可为空!")
private String ysje;
@NotEmpty(message = "支付方式不可为空!")
private String paytype;
@NotEmpty(message = "支付金额不可为空!")
private String paymoney;
@NotEmpty(message = "支付流水号不可为空!")
private String paylsh;
@NotEmpty(message = "医保报销金额不可为空!")
private String ybzf;
/**
* 支付时间
*/
private String paytime;
}

View File

@@ -5,9 +5,13 @@ import com.dpkj.common.dto.LexMarkResultDTO;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.print.request.ReceiptPrintRequest;
import com.dpkj.modules.print.service.PrintService;
import com.dpkj.modules.print.vo.PrinterStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;

View File

@@ -8,7 +8,7 @@ import lombok.Getter;
public enum ReceiptTemplateEnum {
/**
* 自挂号模板
* 自挂号模板
*/
REGISTER("1", "register", "自助挂号的"),
@@ -25,9 +25,7 @@ public enum ReceiptTemplateEnum {
/**
* 门诊缴费的模板
*/
OUTPATIENT_T2("4", "department", "门诊缴费的小票-T2"),
;
OUTPATIENT_T2("4", "department", "门诊缴费的小票-T2");
private final String code;
@@ -36,7 +34,7 @@ public enum ReceiptTemplateEnum {
private final String desc;
ReceiptTemplateEnum(String code, String templateName, String desc){
ReceiptTemplateEnum(String code, String templateName, String desc) {
this.code = code;
this.templateName = templateName;
this.desc = desc;
@@ -44,10 +42,11 @@ public enum ReceiptTemplateEnum {
/**
* 通过code获取模板名称
*
* @param code code/也有可能直接是一个名称
* @return 模板的名称
*/
public static String getTemplateName(String code){
public static String getTemplateName(String code) {
if (code == null || "".equals(code)) {
throw new RRException("模板名称不能为空");
}
@@ -55,7 +54,7 @@ public enum ReceiptTemplateEnum {
String name = null;
for (ReceiptTemplateEnum enumEntity : ReceiptTemplateEnum.values()) {
String enumCode = enumEntity.getCode();
if ( enumCode.equals(code)){
if (enumCode.equals(code)) {
name = enumEntity.getTemplateName();
break;
}

View File

@@ -53,7 +53,7 @@ public class ReceiptPrintRequest implements Serializable {
/**
* 生成的模板的高度默认为1200
*/
private Integer height = 1200;
private Integer height = 1250;
}

View File

@@ -1,6 +1,7 @@
package com.dpkj.modules.print.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.config.PrinterConfig;
import com.dpkj.common.dto.LexMarkDTO;
import com.dpkj.common.dto.LexMarkResultDTO;
import com.dpkj.common.exception.RRException;
@@ -8,12 +9,14 @@ import com.dpkj.common.utils.ThirdService;
import com.dpkj.modules.print.enums.*;
import com.dpkj.modules.print.request.MS439Request;
import com.dpkj.modules.print.service.MS439PrintService;
import com.dpkj.modules.print.utils.FolderUtils;
import com.dpkj.modules.print.utils.PDFUtils;
import com.dpkj.modules.print.vo.PrinterStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
import java.io.File;
/**
* 利盟MS439打印机服务
@@ -29,8 +32,19 @@ public class MS439PrintServiceImpl implements MS439PrintService {
@Resource
private ThirdService thirdService;
@Resource
private PrinterConfig printerConfig;
@Override
public LexMarkResultDTO<?> printImage(MS439Request request) {
FolderUtils.checkFolderAndCreate(request.getFileDir());
// 校验是本地地址还是http开头的
PDFUtils.AddressType addressType = PDFUtils.checkAddressType(request.getFileDir());
if (addressType.equals(PDFUtils.AddressType.HTTP)) {
request.setFileDir(PDFUtils.downloadPdf(request.getFileDir()));
}
LexMarkResultDTO<PrinterStatus> status = this.getStatus(request.getPagesource());
PrinterStatus ms439 = status.getData();
@@ -64,9 +78,20 @@ public class MS439PrintServiceImpl implements MS439PrintService {
// param.put("prtData", String.format("PrintType=%d;pagesource=%s;copies=%d;file[0]=%s;stamp=%d;duplex=%d;color=%d;direction=%d",
// request.getPagesource(), request.getCopies(), request.getFileDir(), request.getStamp(),
// request.getDuplex(), request.getColor(), request.getDirection()));
// 计算A4/A5归属
int printType = 1; // 默认使用第一层
if (printerConfig.getLevelOne().equals("A5")) {
if (request.getPagesource().equals("A4")) {
printType = 2;
}
} else if (printerConfig.getLevelOne().equals("A4")) {
if (request.getPagesource().equals("A5")) {
printType = 2;
}
}
param.put("prtData", String.format("PaperNum=%d;PrintType=%d;Stamp=%d;File[0]=%s;WaitNum=%d;copies=%d;stamp=%d;duplex=%d;color=%d;direction=%d",
request.getCopies(), // 盖章事件分数,和打印份数一致
Objects.equals(request.getPagesource(), "A4") ? 1 : 2, // 打印类型1-A4或者2-A5
printType, // 打印类型1-A4或者2-A5
request.getStamp(), // 是否盖章
request.getFileDir(), // 要打印的文件的路径
request.getCopies(), // 盖章事件分数,和打印份数一致
@@ -78,7 +103,23 @@ public class MS439PrintServiceImpl implements MS439PrintService {
lexMarkDTO.setParam(param.toString());
LexMarkResultDTO<LexMarkResultDTO.Param> paramLexMarkResultDTO = thirdService.callDevice(lexMarkDTO, LexMarkResultDTO.Param.class);
// thirdService.close("HtmPrinter");
if ( paramLexMarkResultDTO.getData().getResult() != 0){
throw new RRException(500, paramLexMarkResultDTO.getData().getResult() + "");
}
File file = new File(request.getFileDir());
// 检查文件是否存在
if (file.exists()) {
// 尝试删除文件
if (file.delete()) {
log.info("文件删除成功: " + request.getFileDir());
} else {
log.info("文件删除失败: " + request.getFileDir());
}
} else {
log.info("文件不存在: " + request.getFileDir());
}
return paramLexMarkResultDTO;
}
@@ -107,11 +148,11 @@ public class MS439PrintServiceImpl implements MS439PrintService {
String paperStatus = "";
// 如果是A4,当前是取第一层纸盒作为A4
if (papersource.equals("A4")) {
paperStatus = papers[0];
paperStatus = printerConfig.getLevelOne().equals("A4") ? papers[0] : papers[1];
}
// 如果是A5当前是取第二层纸盒只作为A5
if (papersource.equals("A5")) {
paperStatus = papers[1];
paperStatus = printerConfig.getLevelOne().equals("A5") ? papers[0] : papers[1];
}
if ( !(paperStatus.equals(MS439PaperStatusEnum.PAPERFULL.getPrintCode()) || paperStatus.equals(MS439PaperStatusEnum.PAPERLOW.getPrintCode()))){
throw new RRException(500, paperStatus);

View File

@@ -1,7 +1,9 @@
package com.dpkj.modules.print.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.config.PrinterConfig;
import com.dpkj.common.dto.LexMarkDTO;
import com.dpkj.common.dto.LexMarkResultDTO;
import com.dpkj.common.exception.RRException;
@@ -9,6 +11,7 @@ import com.dpkj.common.utils.TemplateUtils;
import com.dpkj.common.utils.ThirdService;
import com.dpkj.modules.print.enums.*;
import com.dpkj.modules.print.service.PrintService;
import com.dpkj.modules.print.utils.FolderUtils;
import com.dpkj.modules.print.vo.PrinterStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -19,6 +22,9 @@ import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 挂号服务打印
@@ -31,49 +37,110 @@ import java.io.IOException;
@Service("registerService")
public class RegisterServiceImpl implements PrintService {
@Resource
private PrinterConfig printerConfig;
@Resource
private ThirdService thirdService;
private static final int MAX_HEIGHT = 1000;
private static final int FIXED_WIDTH = 690;
private static final int FIXED_WIDTH = 730;
@Override
public LexMarkResultDTO<LexMarkResultDTO.Param> printImage(JSONObject data, String template, int width, int height, String saveDir) {
if ( width > 690 || height <= 0){
width = 690;
FolderUtils.checkFolderAndCreate(saveDir);
if (height <= 0) {
width = FIXED_WIDTH;
}
if (StringUtils.isEmpty(printerConfig.getTimeType())) {
throw new RRException("获取打印时间格式出错:" + printerConfig.getTimeType());
}
// if ( StringUtils.isEmpty(printerConfig.getTerminalNumber())){
// throw new RRException("获取终端号出错:" + printerConfig.getTerminalNumber());
// }
// 强行设置终端号
// data.put("terminalNumber", printerConfig.getTerminalNumber());
// 强行设置终端号和打印时间
SimpleDateFormat sdf = new SimpleDateFormat(printerConfig.getTimeType());
String formattedDate = sdf.format(new Date());
data.put("printTime", formattedDate);
// 获取模板
String templateName = ReceiptTemplateEnum.getTemplateName(template);
int dinyHeight = 0;
if (templateName.equals("department")) {
// 由于是使用的门诊小票-T2那么默认的高度为1000强行设置通过动态修改渲染的图片的高度
height = 1000;
// 单行最大长度为10
int singleLineMaxLength = 10;
// 这里的长度取自于department.html模板中的项目单个tr高度并且略高于该高度
int singleLineHeight = 40;
// 动态计算长度
JSONArray items = data.getJSONArray("items");
for (Object item : items) {
JSONObject itemEntity = (JSONObject) item;
String projectName = String.valueOf(itemEntity.get("name"));
int length = projectName.length();
int count = (int) Math.ceil((double) length / singleLineMaxLength);
dinyHeight += count * singleLineHeight;
}
// 计算是否有门诊号
String outpatientNumber = data.getString("outpatientNumber");
if (!StringUtils.isEmpty(outpatientNumber)) {
dinyHeight += 35;
}
// 计算是否有就诊医生
String doctor = data.getString("doctor");
if (!StringUtils.isEmpty(doctor)) {
dinyHeight += 35;
}
// 计算是否有就诊科室
String department = data.getString("department");
if (!StringUtils.isEmpty(department)) {
dinyHeight += 35;
}
}
height += dinyHeight;
this.getStatus();
StringBuilder filePath = new StringBuilder(saveDir);
// 校验是否选中了模板,如果没选中模板的话则不需要另外生成了
if ( !StringUtils.isEmpty(templateName) && !StringUtils.isEmpty(saveDir)){
if (!StringUtils.isEmpty(templateName) && !StringUtils.isEmpty(saveDir)) {
byte[] image = new TemplateUtils().generateReceiptImage(data, templateName, width, height, filePath);
}else {
} else {
throw new RRException("模板渲染错误");
}
String[] deletePathList = new String[(int) Math.ceil((double) height / MAX_HEIGHT) + 1];
// 计算切割的块数
int numPieces = (int) Math.ceil((double) height / MAX_HEIGHT);
String[] deletePathList = new String[numPieces + 1];
deletePathList[0] = filePath.toString();
String[] filePathList = new String[(int) Math.ceil((double) height / MAX_HEIGHT)];
String[] filePathList = new String[numPieces];
// 对图片进行分块处理,当前台式打印机最大参数配置 宽度690高度1200
if ( height > MAX_HEIGHT){
if (height > MAX_HEIGHT) {
try {
// 读取输入图片
BufferedImage originalImage = ImageIO.read(new File(filePath.toString()));
// 计算切割的块数
int numPieces = (int) Math.ceil((double) height / MAX_HEIGHT);
// 循环切割图片并保存每一块
for (int i = 0; i < numPieces; i++) {
int startY = i * MAX_HEIGHT;
int pieceHeight = Math.min(MAX_HEIGHT, height - startY);
// 创建一个新的 BufferedImage 对象,用于存储切割的部分
BufferedImage piece = new BufferedImage(FIXED_WIDTH, pieceHeight, BufferedImage.TYPE_INT_RGB);
piece.getGraphics().drawImage(originalImage, 0, 0, FIXED_WIDTH, pieceHeight, 0, startY, FIXED_WIDTH, startY + pieceHeight, null);
BufferedImage piece = new BufferedImage(width, pieceHeight, BufferedImage.TYPE_INT_RGB);
piece.getGraphics().drawImage(originalImage, 0, 0, width, pieceHeight, 0, startY, width, startY + pieceHeight, null);
// 保存切割的图片块
File outputFile = new File(saveDir + "/output_" + (i + 1) + ".jpg");
@@ -83,10 +150,10 @@ public class RegisterServiceImpl implements PrintService {
}
} catch (IOException e) {
log.error("图片分段错误");
log.error("模板切割失败");
e.printStackTrace();
}
}else {
} else {
filePathList[0] = "LOGO1=" + filePath;
}
@@ -108,7 +175,7 @@ public class RegisterServiceImpl implements PrintService {
LexMarkResultDTO<LexMarkResultDTO.Param> paramLexMarkResultDTO = thirdService.callDevice(lexMarkDTO, LexMarkResultDTO.Param.class);
for (String path : deletePathList) {
if ( path != null && !path.isEmpty()) {
if (path != null && !path.isEmpty()) {
File file = new File(path);
// 检查文件是否存在
if (file.exists()) {
@@ -124,7 +191,6 @@ public class RegisterServiceImpl implements PrintService {
}
}
// this.thirdService.close("ReceiptPrinter");
return paramLexMarkResultDTO;
}
@@ -137,7 +203,7 @@ public class RegisterServiceImpl implements PrintService {
lexMarkDTO.setPluginMethod("getInfo");
// lexMarkDTO.setMethodType(0);
LexMarkResultDTO<PrinterStatus> status = thirdService.callDevice(lexMarkDTO, PrinterStatus.class);
if ( status.getResult() != 0){
if (status.getResult() != 0) {
thirdService.open("ReceiptPrinter", 1);
status = thirdService.callDevice(lexMarkDTO, PrinterStatus.class);
}
@@ -149,16 +215,16 @@ public class RegisterServiceImpl implements PrintService {
throw new RRException("获取打印机纸张状态出问题");
}
String[] papers = stPaperEx.split("\\|");
if ( papers.length < 1) {
if (papers.length < 1) {
throw new RRException("打印机纸盒数量不对");
}
String paperStatus = papers[0];
if ( !(paperStatus.equals(MS439PaperStatusEnum.PAPERFULL.getPrintCode()) || paperStatus.equals(MS439PaperStatusEnum.PAPERLOW.getPrintCode()))){
if (!(paperStatus.equals(MS439PaperStatusEnum.PAPERFULL.getPrintCode()) || paperStatus.equals(MS439PaperStatusEnum.PAPERLOW.getPrintCode()))) {
throw new RRException(500, paperStatus);
}
// 校验打印机是否正常,除了HEALTHY都抛异常
if (!ms439.getStDeviceStatus().equals(MS439DeviceStatusEnum.HEALTHY.getPrintCode()) ) {
if (!ms439.getStDeviceStatus().equals(MS439DeviceStatusEnum.HEALTHY.getPrintCode())) {
throw new RRException(500, ms439.getStDeviceStatus());
}
@@ -168,13 +234,13 @@ public class RegisterServiceImpl implements PrintService {
}
// 校验磁带 满或者少才放行
if ( false && !(ms439.getStToner().equals(MS439TonerStatusEnum.TONERFULL.getPrintCode()) || ms439.getStToner().equals(MS439TonerStatusEnum.TONERLOW.getPrintCode()))){
if (false && !(ms439.getStToner().equals(MS439TonerStatusEnum.TONERFULL.getPrintCode()) || ms439.getStToner().equals(MS439TonerStatusEnum.TONERLOW.getPrintCode()))) {
throw new RRException(500, ms439.getStToner());
}
// 校验墨盒 满或者少才放行,当前获取为未知
if ( false && !(ms439.getStInk().equals(MS439InkStatusEnum.INKFULL.getPrintCode()) || ms439.getStInk().equals(MS439InkStatusEnum.INKLOW.getPrintCode())
|| ms439.getStInk().equals(MS439InkStatusEnum.INKUNKNOWN.getPrintCode()))){
if (false && !(ms439.getStInk().equals(MS439InkStatusEnum.INKFULL.getPrintCode()) || ms439.getStInk().equals(MS439InkStatusEnum.INKLOW.getPrintCode())
|| ms439.getStInk().equals(MS439InkStatusEnum.INKUNKNOWN.getPrintCode()))) {
throw new RRException(500, ms439.getStInk());
}

View File

@@ -0,0 +1,40 @@
package com.dpkj.modules.print.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
/**
* 文件夹处理
*
* @author <a href="https://gitee.com/shi-chongli">石头人</a>
* @version 1.0
* @since 2025-03-31 17:28:34
*/
@Slf4j
public class FolderUtils {
public static void checkFolderAndCreate(String folderPath){
File fileOrFolder = new File(folderPath);
String targetPath;
if (fileOrFolder.isFile()) {
// 如果是文件,获取文件所在目录路径
targetPath = fileOrFolder.getParent();
} else {
// 如果不是文件(可能是不存在的文件夹或已存在的文件夹),使用原始路径
targetPath = folderPath;
}
File targetFolder = new File(targetPath);
if (!targetFolder.exists()) {
boolean success = targetFolder.mkdirs();
if (success) {
log.info("文件夹创建成功: " + targetPath);
} else {
log.error("文件夹创建失败: " + targetPath);
}
}
}
}

View File

@@ -0,0 +1,88 @@
package com.dpkj.modules.print.utils;
import com.dpkj.common.exception.RRException;
import lombok.extern.slf4j.Slf4j;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
@Slf4j
public class PDFUtils {
private static final String defaultPath = "D:/images";
public enum AddressType {
HTTP,
LOCAL,
OTHER,
UNKNOWN
}
/**
* 校验地址类型
* @param address 地址
*/
public static AddressType checkAddressType(String address) {
if (address == null || address.isEmpty()) {
return AddressType.UNKNOWN;
}
// 检查是否为 HTTP 或 HTTPS 地址
if (address.startsWith("http://") || address.startsWith("https://")) {
return AddressType.HTTP;
}
// 简单判断是否为本地地址
// 本地地址可能以文件协议 file:// 开头,或者是一个相对路径或绝对路径
if (address.startsWith("file://") ||
(address.contains(":") && address.contains("\\")) ||
(address.startsWith("/"))) {
return AddressType.LOCAL;
}
return AddressType.OTHER;
}
public static String downloadPdf(String pdfUrl) {
String savePath = defaultPath + "/genera_image_" + System.currentTimeMillis() + ".pdf";
try {
// 创建 URL 对象
URL url = new URL(pdfUrl);
// 打开 HTTP 连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方法为 GET
connection.setRequestMethod("GET");
// 获取响应码
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 获取输入流
try (InputStream inputStream = connection.getInputStream();
// 创建文件输出流
FileOutputStream outputStream = new FileOutputStream(savePath)) {
byte[] buffer = new byte[4096];
int bytesRead;
// 循环读取数据并写入文件
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
log.info("PDF 文件下载成功,保存路径: " + savePath);
}
} else {
log.error("下载失败,响应码: " + responseCode);
}
// 断开连接
connection.disconnect();
}catch (Exception e) {
throw new RRException("PDF文件下载失败");
}
return savePath;
}
}

View File

@@ -295,7 +295,7 @@ public class ReadCardServiceImpl implements ReadCardService {
socialSecurityCardReader.setCallID(19256);
socialSecurityCardReader.setDevName(LexMarkConst.CARD_READER);
JSONObject socialSecurityCardReaderParam = new JSONObject();
socialSecurityCardReaderParam.put("iType", 3);
socialSecurityCardReaderParam.put("iType", 1);
socialSecurityCardReader.setParam(socialSecurityCardReaderParam.toJSONString());
log.info("[ReadCardServiceImpl][connectAndSocialSecurityCardReader][300]: 调用读取社保卡信息方法,参数: {}", socialSecurityCardReader);
IDCardReadResultVO socialSecurityCardReaderResult = thirdServiceUtil.callDevice(socialSecurityCardReader, IDCardReadResultVO.class);

View File

@@ -0,0 +1,52 @@
package com.dpkj.modules.scanface.ali.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @description: 支付宝刷脸配置信息
* @author: Zhangxue
* @time: 2025/4/16 20:23
*/
@Data
@Component
@ConfigurationProperties(prefix = "dpkj.ali.face")
public class AliFaceConfig {
/**
* ABCP_SDK部署后DLL文件路径
*/
private String dllPath;
/**
* 应用管理-appid
*/
private String appId;
/**
* IOT应用版本
*/
private String appVersion;
/**
* 签约商家的 PID以 2088 开头,企业主体
*/
private String merchantId;
/**
* 商家机具终端编号,每台设备保持唯一
*/
private String deviceNum;
/**
* 服务商的 PID
*/
private String partnerId;
/**
* 核心入参 serviceId
*/
private String serviceId;
}

View File

@@ -0,0 +1,25 @@
package com.dpkj.modules.scanface.ali.constants;
/**
* @description: 支付宝刷脸模块常量
* @author: Zhangxue
* @time: 2025/4/24 9:25
*/
public final class AliFaceConstants {
/**
* 刷脸去初始化服务
* https://ant-iot.alipay.com/open/iotbpaas/service/serviceManage/serviceDetail
*/
public static final String SMILEVERIFYNIN_V1 = "BPaaSSmileVerifyNonInitV1";
/**
* 刷脸支付服务
* <a href="https://ant-iot.alipay.com/open/iotbpaas/app/appManage/newAppPublish/serviceDetail">...</a>
*/
public static final String SMILEPAY = "BPaaSFaceSmilePayVerify";
}

View File

@@ -0,0 +1,114 @@
package com.dpkj.modules.scanface.ali.controller;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.scanface.ali.config.AliFaceConfig;
import com.dpkj.modules.scanface.ali.constants.AliFaceConstants;
import com.dpkj.modules.scanface.ali.dll.AbcpInvoke;
import com.dpkj.modules.scanface.ali.service.IAliScanFaceService;
import com.dpkj.modules.scanface.ali.vo.AliOrderVo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.File;
import java.util.Map;
/**
* @Auther: 萧道子
* @Date: 2025/4/16 16:47
* @Description:
*/
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/scanface/ali")
public class AliScanFaceController {
private final IAliScanFaceService aliScanFaceService;
@Resource
private AliFaceConfig aliFaceConfig;
/**
* 完整的统一的支付宝刷脸支付
* 1、调用硬件刷脸获取到ftoken
* 2、调用后端的支付宝支付、存入hispay
* @param aliOrderVo
* @return
* @throws Exception
*/
@PostMapping("/aliFacePay")
public Result<JSONObject> aliFacePay(@RequestBody AliOrderVo aliOrderVo) throws Exception {
return aliScanFaceService.aliFacePay(aliOrderVo);
}
/**
* ABCP初始化
* 文档地址https://opendocs.alipay.com/iot/05e9ye
* ABCP初始化商家App 启动时通过调用接口 abcp_init 执行 ABCP SDK 的初始化,初始化动作只需启动时调用一次,初始化的时候需保证机具网络处于联网状态。
*
* @return
*/
@GetMapping("iniAbcp")
Result<Object> iniAbcp() {
return aliScanFaceService.iniAbcpAbsolute();
}
/**
* ABCP服务调用 刷脸去初始化服务,获取ftoken
* 文档地址https://opendocs.alipay.com/iot/05e9ye
* 初始化成功后商家App 可根据业务需求,调用接口 abcp_start_service 执行 ABCP 所提供的服务。
* 上述服务调用过程可重复多次调用,通过传入不同的 service_code 来调用不同的 ABCP 服务。
*
* @return
*/
@GetMapping("abcpStartServiceIni")
Result<Object> abcpStartServiceIni() {
aliScanFaceService.iniAbcpAbsolute();
//参数
JSONObject zolozConfig = new JSONObject().fluentPut("installAngle",90);
JSONObject params = new JSONObject()
.fluentPut("serviceId", aliFaceConfig.getServiceId())
.fluentPut("zolozConfig",zolozConfig);
String json = params.toJSONString();
String service_code = AliFaceConstants.SMILEVERIFYNIN_V1; //调用的组件编码:初始化
return aliScanFaceService.startServiceIni(json, service_code);
}
/**
* ABCP服务停用
* 终止正在进行中的某次service_code + traceId或某类service_code服务调用
* 如处于支付环节,此次服务调用将不接受停止。
* @param traceId
* @return
*/
@PostMapping("stopService")
Result<Object> stopService(@RequestParam String traceId) {
aliScanFaceService.iniAbcpAbsolute();
//参数
JSONObject params = new JSONObject()
.fluentPut("traceId",traceId);
//.fluentPut("service_code",AliFaceConstants.SMILEVERIFYNIN_V1); //
String json = params.toJSONString();
String service_code = AliFaceConstants.SMILEVERIFYNIN_V1; //调用的组件编码
return aliScanFaceService.stopService(json, service_code);
}
}

View File

@@ -0,0 +1,288 @@
package com.dpkj.modules.scanface.ali.dll;
import com.dpkj.common.vo.Result;
import com.sun.jna.Callback;
import com.sun.jna.Native;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class AbcpInvoke {
public interface CallbackStr {
public void OnString(String result);
}
public interface CallbackRsp {
public void OnProcess(int code, String subCode, String subMsg, String result);
public void OnFinish(int code, String subCode, String subMsg, String result);
}
public static void SetAPIPathFile(String fileDylib) {
if (msAbcpNativeDll == null) {
File file = new File(fileDylib);
if (file.exists()) {
try {
msAbcpNativeDll = (AbcpNativeDll) Native.load(fileDylib, AbcpNativeDll.class);
} catch (Throwable e) {
msAbcpNativeDll = null;
log.error("## 本地库未加载,返回错误 ERROR : load. ## [%s][%s] %n", fileDylib, e.getMessage());
}
} else {
log.error("## ERROR : dylib NOT exist ## %s %n", fileDylib);
}
}
}
public static void AbcpInit(String appId, String appVersion, String json, CallbackRsp callback) {
if (msAbcpNativeDll == null) {
callback.OnFinish(3, "E69001", "[java] api load failed!", "");
return;
}
appId = FixNullString(appId); // involved null ptr!
appVersion = FixNullString(appVersion); // involved null ptr!
json = FixNullString(json); // involved null ptr!
msLock.lock();
Integer seedId = msSeedId++;
msMapAbcpReponse.put(seedId, callback);
msLock.unlock();
try {
msAbcpNativeDll.bp_api_set_isv_lan(3);// 0:cpp | 1:cshap | 2:jni | 3:jna
} catch (Throwable e) {
log.error("## ERROR ## %s %n", e.getMessage());
}
try {
msAbcpNativeDll.abcp_init(seedId, appId, appVersion, json, msABCPProcess, msABCPFinish);
} catch (Throwable e) {
callback.OnFinish(3, "E69001", e.getMessage(), "");
log.error("## ERROR ## %s %n", e.getMessage());
}
}
public static void AbcpGetMetaInfo(String appId, String json, CallbackRsp callback) {
if (msAbcpNativeDll == null) {
callback.OnFinish(3, "E69001", "[java] api load failed!", "");
return;
}
appId = FixNullString(appId); // involved null ptr!
json = FixNullString(json); // involved null ptr!
msLock.lock();
Integer seedId = msSeedId++;
msMapAbcpReponse.put(seedId, callback);
msLock.unlock();
try {
msAbcpNativeDll.abcp_get_meta_info(seedId, appId, json, msABCPProcess, msABCPFinish);
} catch (Throwable e) {
callback.OnFinish(3, "E69001", e.getMessage(), "");
log.error("## ERROR ## %s %n", e.getMessage());
}
}
public static void AbcpStartService(String appId, String serviceCode, String json, CallbackRsp callback) {
if (msAbcpNativeDll == null) {
callback.OnFinish(3, "E69001", "[java] api load failed!", "");
return;
}
appId = FixNullString(appId); // involved null ptr!
serviceCode = FixNullString(serviceCode); // involved null ptr!
json = FixNullString(json); // involved null ptr!
Integer seedId;
msLock.lock();
try {
seedId = msSeedId++;
msMapAbcpReponse.put(seedId, callback);
log.info("注册回调: seedId={}", seedId);
} finally {
msLock.unlock();
}
try {
log.info("调用 abcp_start_service: seedId={}, appId={}, serviceCode={}, jsonLength={}", seedId, appId, serviceCode, json.length());
msAbcpNativeDll.abcp_start_service(seedId, appId, serviceCode, json, msABCPProcess, msABCPFinish);
} catch (Throwable e) {
log.error("##本地方法调用异常 ERROR ## %s %n", e.getMessage());
callback.OnFinish(3, "E69001", e.getMessage(), "");
}
}
public static void AbcpTaskStopService(String appId, String serviceCode, String json, CallbackRsp callback) {
if (msAbcpNativeDll == null) {
callback.OnFinish(3, "E69001", "[java] api load failed!", "");
return;
}
appId = FixNullString(appId); // involved null ptr!
serviceCode = FixNullString(serviceCode); // involved null ptr!
json = FixNullString(json); // involved null ptr!
msLock.lock();
Integer seedId = msSeedId++;
msMapAbcpReponse.put(seedId, callback);
msLock.unlock();
try {
log.info("[调用abcp_stop_service]:seedId={}, appId={}, serviceCode={}, json={}", seedId, appId, serviceCode, json);
msAbcpNativeDll.abcp_stop_service(seedId, appId, serviceCode, json, msABCPProcess, msABCPFinish);
} catch (Throwable e) {
callback.OnFinish(3, "E69001", e.getMessage(), "");
log.error("## ERROR ## %s %n", e.getMessage());
}
}
public static void AbcpZimIdInitTest(String metainfo, String smileType, String smileFlag, CallbackStr callback) {
if (msAbcpNativeDll == null) {
callback.OnString("{}");
return;
}
metainfo = FixNullString(metainfo); // involved null ptr!
smileType = FixNullString(smileType); // involved null ptr!
smileFlag = FixNullString(smileFlag); // involved null ptr!
msLock.lock();
Integer seedId = msSeedId++;
msMapAbcpString.put(seedId, callback);
msLock.unlock();
try {
msAbcpNativeDll.abcp_zimid_init_for_test(seedId, metainfo, smileType, smileFlag, msABCPString);
} catch (Throwable e) {
callback.OnString("{}");
log.error("## ERROR ## %s %n", e.getMessage());
}
}
public static void AbcpTaskSubscribeEvent(String appId, String json, CallbackRsp callback) {
if (msAbcpNativeDll == null) {
callback.OnFinish(3, "E69001", "[java] api load failed!", "");
return;
}
appId = FixNullString(appId); // involved null ptr!
json = FixNullString(json); // involved null ptr!
msLock.lock();
Integer seedId = msSeedId++;
msMapAbcpReponse.put(seedId, callback);
msLock.unlock();
try {
msAbcpNativeDll.abcp_call(seedId, appId, "SubscribeEvent", json, msABCPProcess, msABCPFinish);
} catch (Throwable e) {
callback.OnFinish(3, "E69001", e.getMessage(), "");
log.error("## ERROR ## %s %n", e.getMessage());
}
}
public static void AbcpTaskUnsubscribeEvent(String appId, String json, CallbackRsp callback) {
if (msAbcpNativeDll == null) {
callback.OnFinish(3, "E69001", "[java] api load failed!", "");
return;
}
appId = FixNullString(appId); // involved null ptr!
json = FixNullString(json); // involved null ptr!
msLock.lock();
Integer seedId = msSeedId++;
msMapAbcpReponse.put(seedId, callback);
msLock.unlock();
try {
msAbcpNativeDll.abcp_call(seedId, appId, "UnsubscribeEvent", json, msABCPProcess, msABCPFinish);
} catch (Throwable e) {
callback.OnFinish(3, "E69001", e.getMessage(), "");
log.error("## ERROR ## %s %n", e.getMessage());
}
}
// =====================================
private static AbcpNativeDll msAbcpNativeDll = null;
private static ABCPProcess msABCPProcess = new ABCPProcess();
private static ABCPFinish msABCPFinish = new ABCPFinish();
private static ABCPString msABCPString = new ABCPString();
private static Lock msLock = new ReentrantLock();
private static Integer msSeedId = Integer.valueOf(256);
private static Map<Integer, CallbackStr> msMapAbcpString = new HashMap<Integer, CallbackStr>();
private static Map<Integer, CallbackRsp> msMapAbcpReponse = new HashMap<Integer, CallbackRsp>();
private static String FixNullString(String str) {
if (str.equals(null)) {
return "";
}
return str;
}
public static Integer getMsSeedId() {
return msSeedId;
}
// =====================================
public interface FuncProcess extends Callback {
public void onProcess(int seed_id, int code, String subCode, String subMsg, String result);
}
public interface FuncFinish extends Callback {
public void onFinish(int seed_id, int code, String subCode, String subMsg, String result);
}
public interface FuncString extends Callback {
public void onString(int seed_id, String result);
}
public static class ABCPProcess implements FuncProcess {
public void onProcess(int seed_id, int _code, String subCode, String subMsg, String result) {
log.info("[ABCPProcess][onProcess][243][onProcess][seed_id:{}],[_code:{}],[subMsg:{}],[result:{}]", seed_id, _code, subCode, subMsg, result);
Integer seedId = Integer.valueOf(seed_id);
CallbackRsp callback = null;
msLock.lock();
if (msMapAbcpReponse.containsKey(seedId)) {
callback = msMapAbcpReponse.get(seedId);
}
msLock.unlock();
if (callback != null) {
Integer code = Integer.valueOf(_code);
callback.OnProcess(code, subCode, subMsg, result);
}
}
}
public static class ABCPFinish implements FuncFinish {
public void onFinish(int seed_id, int _code, String subCode, String subMsg, String result) {
//log.info("[ABCPFinish][onFinish][261][ABCPFinish数据][seed_id:{}],[_code:{}],[subMsg:{}],[result:{}]", seed_id, _code, subCode, subMsg, result);
Integer seedId = Integer.valueOf(seed_id);
CallbackRsp callback = null;
msLock.lock();
if (msMapAbcpReponse.containsKey(seedId)) {
callback = msMapAbcpReponse.remove(seedId);
}
msLock.unlock();
if (callback != null) {
Integer code = Integer.valueOf(_code);
callback.OnFinish(code, subCode, subMsg, result);
}
}
}
public static class ABCPString implements FuncString {
public void onString(int seed_id, String result) {
Integer seedId = Integer.valueOf(seed_id);
CallbackStr callback = null;
msLock.lock();
if (msMapAbcpString.containsKey(seedId)) {
callback = msMapAbcpString.remove(seedId);
}
msLock.unlock();
if (callback != null) {
callback.OnString(result);
}
}
}
}

View File

@@ -0,0 +1,22 @@
package com.dpkj.modules.scanface.ali.dll;
import com.sun.jna.Callback;
import com.sun.jna.Library;
public interface AbcpNativeDll extends Library {
void bp_api_set_isv_lan(Integer type_isv_language);// 0:cpp | 1:cshap | 2:jni | 3:jna
void abcp_init(Integer arg, String app_id, String app_version, String json_param, Callback on_process, Callback on_finish);
void abcp_start_service(Integer arg, String app_id, String service_code, String json_param, Callback on_process, Callback on_finish);
void abcp_stop_service(Integer arg, String app_id, String service_code, String json_param, Callback on_process, Callback on_finish);
void abcp_get_meta_info(Integer arg, String app_id, String json_param, Callback on_process, Callback on_finish);
void abcp_stop_smile(Integer arg, String app_id, String json_param, Callback on_process, Callback on_finish);
void abcp_zimid_init_for_test(Integer arg, String zimmetainfo, String smileType, String smileFlag, Callback callback);
void abcp_call(Integer arg, String app_id, String call_method, String json_param, Callback on_process, Callback on_finish);
}

View File

@@ -0,0 +1,47 @@
package com.dpkj.modules.scanface.ali.service;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.scanface.ali.vo.AliOrderVo;
public interface IAliScanFaceService {
/**
* 完整的统一的支付宝刷脸支付
* 1、调用硬件刷脸获取到ftoken
* 2、调用后端的支付宝支付、存入hispay记录
* @param aliOrderVo
* @return
* @throws Exception
*/
Result<JSONObject> aliFacePay(AliOrderVo aliOrderVo);
/**
* 初始化调用阿里的ABCP_SDK部署出来的代码中的API文件
* https://opendocs.alipay.com/iot/05e9ye#ABCP%E5%88%9D%E5%A7%8B%E5%8C%96
*/
Result<Object> iniAbcpAbsolute();
/**
* ABCP服务调用 刷脸去初始化服务,获取ftoken
* 接受调用者传入的参数信息执行指定服务service_code然后通过回调返回服务结果。
* https://opendocs.alipay.com/iot/05e9ye#ABCP%E6%9C%8D%E5%8A%A1%E8%B0%83%E7%94%A8
* @param json 组装参数
* @param service_code 所要调用的组件编码
* @return
*/
Result<Object> startServiceIni(String json,String service_code);
/**
* ABCP服务停用
* 终止正在进行中的某次service_code + traceId或某类service_code服务调用然后通过回调返回服务终止结果
* 如处于支付环节,此次服务调用将不接受停止。
* @param json 组装参数
* @param service_code 组件编码
* @return
*/
Result<Object> stopService(String json,String service_code);
}

View File

@@ -0,0 +1,333 @@
package com.dpkj.modules.scanface.ali.service.impl;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.scanface.ali.config.AliFaceConfig;
import com.dpkj.modules.scanface.ali.constants.AliFaceConstants;
import com.dpkj.modules.scanface.ali.dll.AbcpInvoke;
import com.dpkj.modules.scanface.ali.service.IAliScanFaceService;
import com.dpkj.modules.scanface.ali.vo.AliOrderVo;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;
import javax.annotation.Resource;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Auther: 萧道子
* @Date: 2025/4/16 16:47
* @Description:
*/
@Slf4j
@Service
@Component
public class AliScanFaceServiceImpl implements IAliScanFaceService {
@Value("${dpkj.serverurl}")
private String serverUrl;
@Resource
private AliFaceConfig aliFaceConfig;
/**
* @description: * 完整的统一的支付宝刷脸支付
* 1、调用硬件刷脸获取到ftoken
* 2、调用后端的支付宝支付、存入hispay
* @author: zhangxue
* @date: 2025/5/7 9:45
* @Param [aliOrderVo]
* @return: com.dpkj.common.vo.Result<com.alibaba.fastjson.JSONObject>
*/
@Override
public Result<JSONObject> aliFacePay(AliOrderVo aliOrderVo) {
try {
/**
* 1、获取刷脸"刷脸去初始化服务"的ftoken返回值
*/
this.iniAbcpAbsolute();
//参数
JSONObject zolozConfig = new JSONObject().fluentPut("installAngle", 90);
JSONObject params = new JSONObject()
.fluentPut("serviceId", aliFaceConfig.getServiceId())
.fluentPut("zolozConfig", zolozConfig);
String json = params.toJSONString();
String service_code = AliFaceConstants.SMILEVERIFYNIN_V1; //调用的组件编码:初始化
Result<Object> startServiceIniResult = this.startServiceIni(json, service_code);
log.info("[AliScanFaceServiceImpl][aliFacePay][72][调用ABCP调用刷脸的结果] {}", startServiceIniResult.toString());
if (startServiceIniResult.isSuccess()) {
Map<String, String> res = (Map<String, String>) startServiceIniResult.getResult();
String ftoken = res.get("ftoken");
log.info("[AliScanFaceServiceImpl][aliFacePay][76][调用ABCP调用刷脸的结果的ftoken] {}", ftoken.toString());
/**
* 2、调用后端的支付宝统一收单交易支付接口、存入hisPay
*/
aliOrderVo.setAuthCode(ftoken);//Demo值"fp128d26333fa66e66e7f34c493d30cdh76"
JSONObject serverParams = (JSONObject) JSON.toJSON(aliOrderVo);
String url = serverUrl + "openapi/aliPayOrderApi/createOrder";
log.info("[AliScanFaceServiceImpl][aliFacePay][85][调用后端的支付宝统一收单交易支付接口路径] {}", serverUrl);
log.info("[AliScanFaceServiceImpl][aliFacePay][85][调用后端的支付宝统一收单交易支付接口参数] {}", serverParams.toJSONString());
String req = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.toString(CharsetUtil.CHARSET_UTF_8))
.body(serverParams.toJSONString())
.execute()
.body();
JSONObject serverResult = JSONObject.parseObject(req);
log.info("[AliScanFaceServiceImpl][aliFacePay][93][调用后端的支付宝统一收单交易支付接口结果] {}", serverResult.toString());
return Result.ok(serverResult);
} else {
//调用ABCP 刷脸初始化服务失败
log.error("[AliScanFaceServiceImpl][aliFacePay][299]调用ABCP 刷脸初始化服务失败 {}", startServiceIniResult.getMessage());
return Result.error(startServiceIniResult.getMessage());
}
} catch (Exception e) {
e.printStackTrace();
log.error("[AliScanFaceServiceImpl][aliFacePay][302][整个支付宝刷脸模块出现失败:] {}", e.getMessage());
return Result.error("支付宝刷脸失败:" + e.getMessage());
}
}
/**
* @description: 使用绝对路径调用ABCP_SDK部署出来的AbcpInvoke类
* @author: zhangxue
* @date: 2025/5/7 9:45
* @Param []
* @return: com.dpkj.common.vo.Result<java.lang.Object>
*/
@Override
public Result<Object> iniAbcpAbsolute() {
try {
File dllFile = new File(aliFaceConfig.getDllPath());
System.out.println("DLL 存在: " + dllFile.exists());
if (dllFile.exists()) {
//组装参数
JSONObject params = new JSONObject()
.fluentPut("appId", aliFaceConfig.getAppId()) //应用ID
.fluentPut("merchantId", aliFaceConfig.getMerchantId()) //签约商家的 PID以 2088 开头
.fluentPut("deviceNum", aliFaceConfig.getDeviceNum())//商家机具终端编号,每台设备保持唯一
.fluentPut("partnerId", aliFaceConfig.getPartnerId());//服务商的 PID
String json = params.toJSONString();
//指定支付宝LOT SDK的本地库路径
AbcpInvoke.SetAPIPathFile(aliFaceConfig.getDllPath());
//创建回调实例
AbcpInvoke.CallbackRsp callbackRsp = new AbcpInvoke.CallbackRsp() {
@Override
public void OnProcess(int code, String subCode, String subMsg, String result) {
log.info("[AliScanFaceServiceImpl][OnProcess][123][ABCP调用iniAbcpAbsolute][code:{}][subCode:{}][subMsg:{}][result:{}]", code, subCode, subMsg, result);
}
@Override
public void OnFinish(int code, String subCode, String subMsg, String result) {
log.info("[AliScanFaceServiceImpl][OnFinish][128][ABCP调用iniAbcpAbsolute][code:{}][subCode:{}][subMsg:{}][result:{}]", code, subCode, subMsg, result);
}
};
//初始化
AbcpInvoke.AbcpInit(aliFaceConfig.getAppId(), aliFaceConfig.getAppVersion(), json, callbackRsp);
return Result.ok("支付宝ABCP初始化成功");
} else {
log.info("[AliScanFaceServiceImpl][iniAbcpAbsolute][55][{} :文件不存在]", aliFaceConfig.getDllPath());
return Result.error("支付宝ABCP初始化失败" + aliFaceConfig.getDllPath() + "不存在");
}
} catch (Exception e) {
e.printStackTrace();
return Result.error("初始化失败");
}
}
/**
* ABCP服务调用 刷脸初始化服务,获取ftoken
* 接受调用者传入的参数信息执行指定服务service_code然后通过回调返回服务结果。
* https://opendocs.alipay.com/iot/05e9ye#ABCP%E6%9C%8D%E5%8A%A1%E8%B0%83%E7%94%A8
*
* @param json 组装参数
* @param service_code 所要调用的组件编码
* @return
*/
@Override
public Result<Object> startServiceIni(String json, String service_code) {
try {
String appId = aliFaceConfig.getAppId();
// 使用 CountDownLatch 实现线程同步
CountDownLatch latch = new CountDownLatch(1);
CountDownLatch latchFinish = new CountDownLatch(1);
AtomicReference<Result<Object>> finishResultRef = new AtomicReference<>();
//获取返回数据
AtomicReference<Integer> processCode = new AtomicReference<>();
AtomicReference<String> processResult = new AtomicReference<>();
AtomicReference<Integer> finishCode = new AtomicReference<>();
AtomicReference<String> finishResult = new AtomicReference<>();
//接收结果
Map<String, String> res = new HashMap<>();
AbcpInvoke.CallbackRsp callbackRsp = new AbcpInvoke.CallbackRsp() {
@Override
public void OnProcess(int code, String subCode, String subMsg, String result) {
log.info("[AliScanFaceServiceImpl][OnProcess][189][service_code:{}][code:{}][subCode:{}][subMsg:{}][result:{}]", service_code, code, subCode, subMsg, result);
try {
processCode.set(code);
processResult.set(result);
} finally {
latch.countDown(); // 确保无论如何都释放锁
}
}
@Override
public void OnFinish(int code, String subCode, String subMsg, String result) {
log.info("[AliScanFaceServiceImpl][OnFinish][200][ABCP服务调用 刷脸初始化服务,获取ftoken:][service_code:{}][code:{}][subCode:{}][subMsg:{}][result:{}]", service_code, code, subCode, subMsg, result);
/**Demo示例记录
* [service_code:BPaaSSmileVerifyNonInitV1][code:1000][subCode:E00000][subMsg:SUCCESS][result:{"code":1000,"subCode":"OK_SUCCESS","subMessage":"SUCCESS","barCode":"281215320962898068","ftoken":"fp1efd3d4c0230a28f5261efe7c5050eh28","alipayUid":"2088812449506047","accountList":"[\"104***@qq.com\"]","authToken":"44686f7195c77ee2e09c09bcdc657dd5h28i","result":{"accountList":["104***@qq.com"],"alipayUid":"2088812449506047","allowRetry":false,"authToken":"44686f7195c77ee2e09c09bcdc657dd5h28i","barCode":"281215320962898068","certName":"您好,*雪","ftoken":"fp1efd3d4c0230a28f5261efe7c5050eh28","type":"selectUid"},"easterEgg":false,"zolozConfig":{"installAngle":90},"serviceId":"pay","traceId":"2444-44-1745802328","callStartTimeMs":1745802328114,"localTime":"2025-04-28-09-05-41-791"}]
*/
try {
finishCode.set(code);
finishResult.set(result);
} finally {
latchFinish.countDown(); // 确保无论如何都释放锁
}
}
};
//调用
log.info("[AliScanFaceServiceImpl][startService][141][ABCP调用刷脸初始化服务-调用AbcpStartService参数][appId:{}][service_code:{}][json:{}] ", appId, service_code, json.toString());
AbcpInvoke.AbcpStartService(appId, service_code, json, callbackRsp);
// 等待process回调完成设置超时避免死锁
boolean awaitSuccess = latch.await(10, TimeUnit.SECONDS);
if (!awaitSuccess) {
log.info("[AliScanFaceServiceImpl][startServiceIni][221][service_code:{}][ABCP调用刷脸:等待process回调超时]", service_code);
return Result.error("等待process回调超时");
} else {
if (processCode.get() == 0) {
JSONObject jsonObject = JSONObject.parseObject(processResult.get());
log.info("[AliScanFaceServiceImpl][startServiceIni][225][service_code:{}][ABCP调用刷脸-等待process回调完成结果] {}", service_code, jsonObject.toString());
if (jsonObject.containsKey("traceId")) {
res.put("traceId", jsonObject.getString("traceId"));
} else {
return Result.error("ABCP调用刷脸初始化服务process失败返回结果无traceId");
}
} else {
return Result.error("ABCP调用刷脸初始化服务process失败");
}
}
// 等待finish回调完成设置超时避免死锁
boolean awaitFinishSuccess = latchFinish.await(120, TimeUnit.SECONDS);
if (!awaitFinishSuccess) {
return Result.error("等待finish回调超时");
} else {
if (finishCode.get() == 1000) {
JSONObject jsonObject = JSONObject.parseObject(finishResult.get());
log.info("[AliScanFaceServiceImpl][startServiceIni][244][service_code:{}] [ABCP调用刷脸-等待finish回调完成结果]{}", service_code, jsonObject.toString());
if (jsonObject.containsKey("ftoken")) {
res.put("ftoken", jsonObject.getString("ftoken"));//ftoken参数的有效期为2分钟
res.put("barCode", jsonObject.getString("barCode"));
finishResultRef.set(Result.ok("ABCP调用刷脸初始化finish服务成功", res));
} else {
return Result.ok("ABCP调用刷脸初始化服务finish失败返回结果无ftoken");
}
} else {
return Result.error("ABCP调用刷脸初始化服务finish失败");
}
}
//结果返回
return finishResultRef.get();
} catch (Exception e) {
e.printStackTrace();
log.info("[AliScanFaceServiceImpl][startServiceIni][261][ABCP调用刷脸初始化服务失败] {}", e.getMessage());
return Result.error("ABCP调用刷脸初始化服务失败" + e.getMessage());
}
}
/**
* ABCP服务停用
* 终止正在进行中的某次service_code + traceId或某类service_code服务调用然后通过回调返回服务终止结果
* 如处于支付环节,此次服务调用将不接受停止。
*
* @param json 组装参数
* @param service_code 组件编码
* @return
*/
@Override
public Result<Object> stopService(String json, String service_code) {
try {
String appId = aliFaceConfig.getAppId();
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Result<Object>> resultRef = new AtomicReference<>();
AtomicReference<Integer> returnCode = new AtomicReference<>();
AtomicReference<String> returnResult = new AtomicReference<>();
AbcpInvoke.CallbackRsp callbackRsp = new AbcpInvoke.CallbackRsp() {
@Override
public void OnProcess(int code, String subCode, String subMsg, String result) {
log.info("[AliScanFaceServiceImpl][261][ABCP服务停用-OnProcess][code:{}][subCode:{}][subMsg:{}][result:{}]", code, subCode, subMsg, result);
}
@Override
public void OnFinish(int code, String subCode, String subMsg, String result) {
log.info("[AliScanFaceServiceImpl][OnFinish][266][ABCP服务停用-OnFinish] [code:{}][subCode:{}][subMsg:{}][result:{}]", code, subCode, subMsg, result);
try {
returnCode.set(code);
returnResult.set(result);
} finally {
latch.countDown(); // 确保无论如何都释放锁
}
}
};
//调用
log.info("[AliScanFaceServiceImpl][stopService][222][调用AbcpTaskStopService参数][appId:{}][service_code:{}][json:{}] ", appId, service_code, json.toString());
AbcpInvoke.AbcpTaskStopService(appId, service_code, json, callbackRsp);
// 等待回调完成(设置超时避免死锁)
boolean awaitSuccess = latch.await(60, TimeUnit.SECONDS);
if (!awaitSuccess) {
return Result.error("等待回调超时");
} else {
if (returnCode.get() == 0) {
System.out.println("停止结果:" + returnResult.get());
} else {
return Result.error("ABCP停止服务失败");
}
}
//结果返回
return resultRef.get();
} catch (Exception e) {
e.printStackTrace();
return Result.error("ABCP服务停用失败" + e.getMessage());
}
}
}

View File

@@ -0,0 +1,81 @@
package com.dpkj.modules.scanface.ali.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @description: 支付宝支付参数,传入统一下单接口
* @author: Zhangxue
* @time: 2025/4/29 14:24
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AliOrderVo implements Serializable {
/**
* 付款模块
*/
private String eventModule;
/**
* 患者Id
*/
private String patientId;
/**
* 商户机具终端编号
*/
private String terminalId;
/**
* 系统订单编号
*/
private String outTradeNo;
/**
* 用户支付金额
*/
//@ApiModelProperty(value = "用户支付金额")
private String totalAmount;
/**
* 订单标题
*/
//@ApiModelProperty(value = "订单标题")
private String subject;
/**
* 支付授权码
* 刷脸标识串fp开头的35位字符串
* 从调用"BPaaSSmileVerifyNonInitV1"服务中获取
*/
//@ApiModelProperty(value = "支付授权码")
private String authCode;
/**
* 支付场景
* bar_code当面付条码支付场景默认值
* security_code当面付刷脸支付场景对应的auth_code为fp开头的刷脸标识串
*/
//@ApiModelProperty(value = "支付授权码")
private String scene;
/**
* 产品码:商家和支付宝签约的产品码
* 当面付场景下,如果签约的是当面付快捷版,则传 OFFLINE_PAYMENT;
* 其它支付宝当面付产品传 FACE_TO_FACE_PAYMENT
* 不传则默认使用FACE_TO_FACE_PAYMENT。
*/
//@ApiModelProperty(value = "订单标题")
private String productCode;
/**
* 卖家支付宝用户ID
*/
//@ApiModelProperty(value = "订单标题")
private String sellerId;
}

View File

@@ -0,0 +1,81 @@
package com.dpkj.modules.scanface.wx.config;
/**
* @description:
* @author: Zhangxue
* @time: 2024/11/28 9:38
*/
public class WechatUrlConfig {
/**
* 获取证书
*/
public static final String CERTIFICATESURL = "https://api.mch.weixin.qq.com/v3/certificates";
/**
* 退款地址
*/
public static final String REFUNDSURL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
/**
* 创建支付分订单
*/
public static final String SERVICEORDER = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder";
/**
* 发起订单支付
*/
public static final String CREATEORDER = "https://api.mch.weixin.qq.com/pay/micropay";
/**
* 查询订单状态
*/
public static final String ORDERQUERY = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 撤销交易
*/
public static final String REVERSE = "https://api.mch.weixin.qq.com/secapi/pay/reverse";
/**
* 取消支付分订单前缀
*/
public static final String ORDERCANCELPREFIX = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder/";
/**
* 取消支付分订单后缀
*/
public static final String ORDERCANCELSUFFIX = "/cancel";
/**
* 完结支付分订单前缀
*/
public static final String SERVICEORDERCOMPLETEPREFIX = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder/";
/**
* 完结支付分订单后缀
*/
public static final String SERVICEORDERCOMPLETESUFFIX = "/complete";
/**
* 获取调用凭证
*/
public static final String GET_WXPAYFACE_AUTHINFO = "https://payapp.weixin.qq.com/face/get_wxpayface_authinfo";
/**
* 查询订单
*/
public static final String FINDBYID = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder";
/**
* 查询退款订单
*/
public static final String FINDREFUND = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/";
}

View File

@@ -0,0 +1,196 @@
package com.dpkj.modules.scanface.wx.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 公众号配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "dpkj.wx")
public class WxMpProperties {
/**
* 多个公众号配置信息
*/
private List<MpConfig> configs;
@Data
public static class MpConfig {
/**
* 设置微信公众号的appid
*/
private String appId;
/**
* 设置微信公众号的app secret
*/
private String secret;
/**
* 设置微信公众号的token
*/
private String token;
/**
* 设置微信公众号的EncodingAESKey
*/
private String aesKey;
/**
* 微信调用接口所需service_id
*/
private String serviceId;
/**
* 微信:商户号
*/
private String mchId;
/**
* 微信支付商户密钥
*/
private String mchKey;
/**
* 微信报文解密V3密钥key
*/
private String v3Key;
/**
* 微信: API密钥
*/
private String keyApi;
/**
* 微信密钥【apiclient_key.pem】地址
p12证书的位置可以指定绝对路径也可以指定类路径以classpath;开头)
*/
private String keyPath;
/**
* 微信商家api序列号
*/
private String mchSerialNo;
/**
* #JSAPI--公众号支付 NATIVE--原生扫码支付 APP--app支付
*/
private String tradeType;
/**
* 微信:支付回调地址
*/
private String payNotifyUrl;
/**
* 微信:退款回调地址
*/
private String refundNotifyUrl;
}
/**
* 微信商户支付配置
*/
private MchConfig mchConfig;
@Data
public static class MchConfig{
/**
* 微信APPID
*/
private String appId;
private String secret;
/**
* 微信:商户号
*/
private String mchId;
//微信支付商户密钥
private String mchKey;
/**
* 微信报文解密V3密钥key
*/
private String v3Key;
/**
* 微信: API密钥
*/
private String keyApi;
/**
* 微信密钥【apiclient_key.pem】地址
p12证书的位置可以指定绝对路径也可以指定类路径以classpath;开头)
*/
private String keyPath;
/**
* 退款证书 private-key-path V3
* apiclient_key.pem 证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateKeyPath;
/**
* 退款证书 private-cert-path
* apiclient_cert.pem 证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateCertPath;
/**
* 微信商家api序列号
*/
private String mchSerialNo;
/**
* 微信调用接口所需service_id
*/
private String serviceId;
/**
* 微信:支付回调地址
*/
private String payNotifyUrl;
/**
* 微信:退款回调地址
*/
private String refundNotifyUrl;
/**
* #JSAPI--公众号支付 NATIVE--原生扫码支付 APP--app支付
*/
private String tradeType;
/**
* NATIVE--原生扫码支付
*/
private String tradeTypeNative;
}
/**
* 访问地址
*/
private MpUrl url;
@Data
public static class MpUrl {
/**
* H5地址
*/
private String h5;
/**
* 接口地址
*/
private String server;
}
}

View File

@@ -0,0 +1,516 @@
package com.dpkj.modules.scanface.wx.controller;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.constant.WxConstant;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.scanface.wx.config.WxMpProperties;
import com.dpkj.modules.scanface.wx.dll.WxpayFaceSDKDll;
import com.dpkj.modules.scanface.wx.service.CallWxpayFaceService;
import com.dpkj.modules.scanface.wx.service.WeChatPayFaceService;
import com.dpkj.modules.scanface.wx.vo.WxFaceOrderVo;
import com.dpkj.modules.scanface.wx.vo.WxFacePayAuthinfoResp;
import com.dpkj.modules.scanface.wx.vo.WxFacePayReq;
import com.dpkj.modules.scanface.wx.vo.WxFacePayResp;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.binarywang.wxpay.bean.request.WxPayOrderReverseRequest;
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.bean.result.WxPayOrderReverseResult;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @description: 微信刷脸支付
* @author: Zhangxue
* @time: 2025/5/28 14:20
*/
@Slf4j
@RestController
@RequestMapping("/wxpayFace")
public class WxFacePayController {
@Resource
private CallWxpayFaceService callWxpayFaceService;
@Resource
private WeChatPayFaceService weChatPayFaceService;
@Value("${dpkj.serverurl}")
private String serverUrl;
//赋值
@Autowired
private WxMpProperties wxMpProperties;
/**
* 调用整个刷脸的完整流程
*
* @param wxFaceOrderVo
* @return
* @throws Exception
*/
@RequestMapping(value = "/doFacePay", method = RequestMethod.POST)
public Result<String> doFacePay(@RequestBody WxFaceOrderVo wxFaceOrderVo) throws Exception {
Result<String> result = new Result<>();
try {
//1 初始化
this.initWxpayface();
//2 获取数据getWxpayfaceRawdata
WxFacePayResp wxFacePayResp = this.getWxpayfaceRawdata();
//String rawdata = wxFacePayResp.getRawdata();
//3、获取调用凭证get_wxpayface_authinfo(rawdata)(获取调用凭证)
WxFacePayAuthinfoResp wxFacePayResp0 = this.getWxpayfaceAuthinfo(wxFacePayResp.getRawdata());
//4、进行人脸识别getWxpayfaceCode获取支付凭证
WxFacePayAuthinfoResp authinfoResp = this.getWxpayfaceCode(wxFaceOrderVo, wxFacePayResp0);
WxPayMicropayResult micropayResult = new WxPayMicropayResult();
if (StringUtil.isNotBlank(authinfoResp.getFace_code())) {
//5、调用后台人脸支付API发起支付
wxFaceOrderVo.setOutTradeNo(getOutTradeNo());
micropayResult = this.toCreateWxOrder(authinfoResp, wxFaceOrderVo, wxFacePayResp0);
//6、向后端查询订单状态 WxPayOrderQueryResult queryResult = queryOrderByNo(micropayResult);
boolean isSuccess = pollOrderStatus(micropayResult);
if (isSuccess) {
//8、更新支付结果updateWxpayfacePayResult
WxFacePayResp updFacePayResp = this.updateWxpayfacePayResult(wxFacePayResp0);
if (ObjUtil.isNotEmpty(updFacePayResp) && WxConstant.STATE_SUCCESS.equals(updFacePayResp.getReturn_code())) {
//11、向后端发起更新订单进行相应的操作、
this.updatePayResult(micropayResult);
result = Result.ok("微信刷脸成功!", micropayResult.getOutTradeNo());
}
} else { //查询失败或者长时间未有结果
//7、撤销交易reverse
WxPayOrderReverseRequest reverseRequest = new WxPayOrderReverseRequest();
reverseRequest.setTransactionId(micropayResult.getTransactionId());
reverseRequest.setOutTradeNo(micropayResult.getOutTradeNo());
WxPayOrderReverseResult reverseResult = this.toReverseOrder(reverseRequest);
result = Result.error("微信刷脸失败后已经撤销订单!");
}
} else {
result = Result.error("获取人脸识别支付凭证为空值");
}
} catch (Exception e) {
e.printStackTrace();
log.info("[WxFacePayController][doFacePay][84][微信刷脸完成流程出错] {}", e.getMessage());
result = Result.error("微信刷脸完成流程出错:" + e.getMessage());
}
return result;
}
/**
* 1 程序启动时初始化 :程序启动时初始化initWxpayface
*
* @return
* @throws JsonProcessingException
*/
@RequestMapping(value = "/initWxpayface", method = RequestMethod.POST)
public WxFacePayResp initWxpayface() throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("initWxpayface", "1", System.currentTimeMillis() / 1000, 1);
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
log.info("[WxFacePayController][initWxpayface][132] [1、程序启动时初始化]{}", wxFacePayResp.toString());
return wxFacePayResp;
}
/**
* 2 获取数据getWxpayfaceRawdata
*
* @return
* @throws JsonProcessingException
*/
public WxFacePayResp getWxpayfaceRawdata() throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("getWxpayfaceRawdata", "1", System.currentTimeMillis() / 1000);
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
log.info("[WxFacePayController][getWxpayfaceRawdata][146][2、获取数据getWxpayfaceRawdata] {}", wxFacePayResp.toString());
return wxFacePayResp;
}
/**
* 3、获取调用凭证get_wxpayface_authinfo(rawdata)(获取调用凭证)
*
* @return
* @throws Exception
*/
public WxFacePayAuthinfoResp getWxpayfaceAuthinfo(String rawdata) throws Exception {
// 构建请求参数的JSON字符串
WxFacePayAuthinfoResp wxFacePayResp = weChatPayFaceService.getWxFaceAuthInfoReqMap(rawdata);
log.info("[WxFacePayController][getWxpayfaceAuthinfo][159][3、获取调用凭证get_wxpayface_authinfo] {}", wxFacePayResp.toString());
return wxFacePayResp;
}
/**
* 4、进行人脸识别getWxpayfaceCode获取支付凭证
*
* @param wxFaceOrderVo
* @return
* @throws Exception
*/
@RequestMapping(value = "/getWxpayfaceCode", method = RequestMethod.POST)
public WxFacePayAuthinfoResp getWxpayfaceCode(@RequestBody WxFaceOrderVo wxFaceOrderVo, WxFacePayAuthinfoResp wxFacePayResp0) throws Exception {
//4、进行人脸识别getWxpayfaceCode获取支付凭证
String outTradeNo = getOutTradeNo();//获取流水号
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("getWxpayfaceCode", "1", System.currentTimeMillis() / 1000);
wxFacePayReq.setAuthinfo(wxFacePayResp0.getAuthinfo())
.setOut_trade_no(outTradeNo)//订单流水号
.setTotal_fee(wxFaceOrderVo.getTotalAmount())//金额:分
/**
* 目标face_code类型可选值"1"刷卡付款码18位数字通过「付款码支付/被扫支付」接口完成支付,
* 如果不填写则默认为"0",人脸付款码:数字字母混合,通过「刷脸支付」接口完成支付。
*/
.setFace_code_type("1");
WxFacePayAuthinfoResp authinfoResp = weChatPayFaceService.getWxpayfaceCode(wxFacePayReq);
log.info("[WxFacePayController][getWxpayfaceCode][189][4、进行人脸识别结果] {}", authinfoResp.toString());
return authinfoResp;
}
/**
* 从后端获取流水号,保证唯一性
*
* @return
*/
private String getOutTradeNo() {
String url0 = serverUrl + "openapi/wxFacePayOrderApi/getOutTradeNo";
log.info("[WxFacePayController][getWxpayfaceCode][112][获取流水号地址] {}", url0);
String req0 = HttpRequest.post(url0)
.header(Header.CONTENT_TYPE, ContentType.JSON.toString(CharsetUtil.CHARSET_UTF_8))
.execute()
.body();
JSONObject tradeNoResult = JSONObject.parseObject(req0);
String outTradeNo = tradeNoResult.getString("result");
log.info("[WxFacePayController][getOutTradeNo][208][获取流水号] {}", outTradeNo);
return outTradeNo;
}
/**
* 5、向后端进行发起订单支付
*
* @return
*/
private WxPayMicropayResult toCreateWxOrder(WxFacePayAuthinfoResp authinfoResp, WxFaceOrderVo wxFaceOrderVo, WxFacePayAuthinfoResp wxFacePayResp0) {
authinfoResp
.setOut_trade_no(wxFaceOrderVo.getOutTradeNo())//订单流水号
.setTotal_fee(wxFaceOrderVo.getTotalAmount())//金额:分
.setPatientId(wxFaceOrderVo.getPatientId())//患者Id
.setEventModule(wxFaceOrderVo.getEventModule()) //付费模块
.setTerminalId(wxFaceOrderVo.getTerminalId()) //商户机具终端编号
.setAuthinfo(wxFacePayResp0.getAuthinfo())
.setNonce_str(wxFacePayResp0.getNonce_str())
.setSign(wxFacePayResp0.getSign());
JSONObject serverParams = (JSONObject) JSON.toJSON(authinfoResp);
String url = serverUrl + "openapi/wxFacePayOrderApi/createFaceOrder";
log.info("[WxFacePayController][getWxpayfaceCode][153][向后台发起微信刷脸订单创建路径] {}", url);
log.info("[WxFacePayController][getWxpayfaceCode][153][调用后台人脸支付API发起支付参数] {}", serverParams.toString());
String req = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.toString(CharsetUtil.CHARSET_UTF_8))
.body(serverParams.toString())
.execute()
.body();
log.info("[WxFacePayController][toCreateWxOrder][237][5、向后台发起进行发起订单支付 结果] {}", req);
JSONObject serverResult = JSONObject.parseObject(req);
Map<String, Object> result = (Map<String, Object>) serverResult.get("result");
log.info("[WxFacePayController][toCreateWxOrder][140][5、向后端进行发起订单支付请求结果] {}", result.toString());
WxPayMicropayResult micropayResult = new WxPayMicropayResult();
log.info("[WxFacePayController][toCreateWxOrder][243][5、调用后台人脸支付API发起支付结果] {}", result.toString());
if (ObjUtil.isNotEmpty(result)) {
micropayResult.setOutTradeNo(result.get("outTradeNo").toString());
micropayResult.setTransactionId(result.get("transactionId").toString());
micropayResult.setTotalFee(Integer.valueOf(result.get("totalFee").toString()));
micropayResult.setReturnCode(result.get("returnCode").toString());
}
return micropayResult;
}
/**
* 轮询逻辑封装方法
* 查询微信刷脸订单状态
* 30秒
* 调用步骤6
*
* @param micropayResult
* @return
*/
public boolean pollOrderStatus(WxPayMicropayResult micropayResult) {
final long timeoutMillis = 30_000; // 30秒超时毫秒
final long pollInterval = 2_000; // 轮询间隔2秒
long startTime = System.currentTimeMillis();
while (true) {
// 1. 查询订单状态
WxPayOrderQueryResult queryResult = this.queryOrderByNo(micropayResult);
log.info("[WxFacePayController][pollOrderStatus][272][1. 查询订单状态:] {}", queryResult);
// 2. 检查成功状态
if (WxConstant.STATE_SUCCESS.equals(queryResult.getReturnCode())) {
log.info("[WxFacePayController][pollOrderStatus][275][订单支付成功!]");
return true;
}
// 3. 检查超时
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed >= timeoutMillis) {
log.info("[WxFacePayController][pollOrderStatus][284][支付超时,未获取成功状态]");
return false;
}
// 4. 等待下一次轮询
try {
long nextPoll = Math.min(pollInterval, timeoutMillis - elapsed);
TimeUnit.MILLISECONDS.sleep(nextPoll);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.info("[WxFacePayController][pollOrderStatus][293] [轮询被中断]");
return false;
}
}
}
/**
* 6、向后端查询订单状态
*
* @param micropayResult
* @return
*/
public WxPayOrderQueryResult queryOrderByNo(WxPayMicropayResult micropayResult) {
WxPayOrderQueryResult queryResult = new WxPayOrderQueryResult();
if (ObjUtil.isNotEmpty(micropayResult)) {
JSONObject serverParams = (JSONObject) JSON.toJSON(micropayResult);
String url = serverUrl + "openapi/wxFacePayOrderApi/queryOrderByNo";
log.info("[WxFacePayController][getWxpayfaceCode][184][6、向后台发起查询订单状态创建路径] {}", url);
log.info("[WxFacePayController][getWxpayfaceCode][186][6、向后端查询订单状态参数] {}", serverParams.toString());
String req = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.toString(CharsetUtil.CHARSET_UTF_8))
.body(serverParams.toString())
.execute()
.body();
log.info("[WxFacePayController][queryOrderByNo][318][6、后台发起查询订单 结果:] {}", req);
JSONObject serverResult = JSONObject.parseObject(req);
Map<String, Object> result = (Map<String, Object>) serverResult.get("result");
log.info("[WxFacePayController][toCreateWxOrder][140][6、后台发起查询订单 请求结果] {}", result.toString());
if (ObjUtil.isNotEmpty(result)) {
queryResult.setReturnCode(result.get("returnCode").toString());
queryResult.setOutTradeNo(result.get("outTradeNo").toString());
queryResult.setTransactionId(result.get("transactionId").toString());
queryResult.setTotalFee(Integer.valueOf(result.get("totalFee").toString()));
}
log.info("[WxFacePayController][toCreateWxOrder][140][ 6、向后端查询订单状态结果] {}", micropayResult.toString());
}
return queryResult;
}
/**
* 7、撤销交易reverse
* 支付交易返回失败或支付系统超时,调用该接口撤销交易。
* 如果此订单用户支付失败,微信支付系统会将此订单关闭;
* 如果用户支付成功,微信支付系统会将此订单资金退还给用户。
*
* @param reverseRequest
* @return
*/
@RequestMapping(value = "/toReverseOrder", method = RequestMethod.POST)
public WxPayOrderReverseResult toReverseOrder(@RequestBody WxPayOrderReverseRequest reverseRequest) {
WxPayOrderReverseResult reverseResult = new WxPayOrderReverseResult();
if (ObjUtil.isNotEmpty(reverseRequest)) {
JSONObject serverParams = (JSONObject) JSON.toJSON(reverseRequest);
String url = serverUrl + "openapi/wxFacePayOrderApi/reverseOrder";
log.info("[WxFacePayController][toReverseOrder][303][向后台发起撤销交易reverse路径] {}", url);
log.info("[WxFacePayController][toReverseOrder][304][调用后台发起撤销交易参数] {}", serverParams.toString());
String req = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.toString(CharsetUtil.CHARSET_UTF_8))
.body(serverParams.toString())
.execute()
.body();
log.info("[WxFacePayController][toReverseOrder][359][向后台发起撤销交易 结果:] {}", req);
JSONObject serverResult = JSONObject.parseObject(req);
reverseResult = (WxPayOrderReverseResult) serverResult.get("result");
log.info("[WxFacePayController][toReverseOrder][314][7、向后台发起撤销交易请求结果] {}", reverseResult.toString());
}
return reverseResult;
}
/**
* 8、更新支付结果updateWxpayfacePayResult
*
* @return
* @throws JsonProcessingException
*/
@RequestMapping(value = "/updateWxpayfacePayResult", method = RequestMethod.POST)
public WxFacePayResp updateWxpayfacePayResult(WxFacePayAuthinfoResp wxFacePayResp0) throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("updateWxpayfacePayResult", "1", System.currentTimeMillis() / 1000);
wxFacePayReq.setAppid(wxMpProperties.getMchConfig().getAppId())
.setMch_id(wxMpProperties.getMchConfig().getMchId())
.setStore_id(WxConstant.STORE_ID)
.setPayresult("SUCCESS")
.setAuthinfo(wxFacePayResp0.getAuthinfo());
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
log.info("[WxFacePayController][updateWxpayfacePayResult][385][8、更新支付结果updateWxpayfacePayResult] {}", wxFacePayResp.toString());
return wxFacePayResp;
}
/**
* 9、停止刷脸支付stopWxpayface
* 仅在人脸凭证/付款码face_code【第4步骤】未返回前可用face_code返回后不可停止
*
* @return
* @throws JsonProcessingException
*/
@RequestMapping(value = "/stopWxpayface", method = RequestMethod.POST)
public WxFacePayResp stopWxpayface(WxFacePayAuthinfoResp wxFacePayResp0) throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("stopWxpayface", "1", System.currentTimeMillis() / 1000);
wxFacePayReq.setAppid(wxMpProperties.getMchConfig().getAppId())
.setMch_id(wxMpProperties.getMchConfig().getMchId())
.setStore_id(WxConstant.STORE_ID)
.setAuthinfo(wxFacePayResp0.getAuthinfo());
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
log.info("[WxFacePayController][stopWxpayface][407][9、停止刷脸支付stopWxpayface的结果] {}", wxFacePayResp.toString());
return wxFacePayResp;
}
/**
* 10、释放资源releaseWxpayface
* 首次刷脸需要initWxpayface刷脸结束后不需要调用releaseWxpayface否则下次刷脸需要重新initWxpayface影响启动耗时
*
* @return
* @throws JsonProcessingException
*/
@RequestMapping(value = "/releaseWxpayface", method = RequestMethod.POST)
public WxFacePayResp releaseWxpayface(WxFacePayAuthinfoResp wxFacePayResp0) throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("releaseWxpayface", "1", System.currentTimeMillis() / 1000);
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
log.info("[WxFacePayController][releaseWxpayface][425][10、释放资源releaseWxpayface的结果] {}", wxFacePayResp.toString());
return wxFacePayResp;
}
/**
* 11、向后端发起更新状态为成功进行相应的操作
*
* @param micropayResult
* @return
*/
public WxPayOrderQueryResult updatePayResult(WxPayMicropayResult micropayResult) {
WxPayOrderQueryResult queryResult = new WxPayOrderQueryResult();
if (ObjUtil.isNotEmpty(micropayResult)) {
JSONObject serverParams = (JSONObject) JSON.toJSON(micropayResult);
String url = serverUrl + "openapi/wxFacePayOrderApi/updatePayResult";
log.info("[WxFacePayController][updatePayResult][184][11、向后端发起更新状态为成功创建路径] {}", url);
log.info("[WxFacePayController][updatePayResult][186][11、向后端发起更新状态为成功参数] {}", serverParams.toString());
String req = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.toString(CharsetUtil.CHARSET_UTF_8))
.body(serverParams.toString())
.execute()
.body();
log.info("[WxFacePayController][updatePayResult][447][11、向后端发起更新状态为成功 结果:] {}", req.toString());
JSONObject jsonObject = JSONObject.parseObject(req);
Map<String, Object> result = (Map<String, Object>) jsonObject.get("result");
log.info("[WxFacePayController][updatePayResult][140][11、向后端发起更新状态为成功 请求结果] {}", result.toString());
}
return queryResult;
}
/**
* 4、进行人脸识别getWxpayfaceCode获取支付凭证
* @return
* @throws Exception
*/
/*@RequestMapping(value = "/getWxpayfaceCode", method = RequestMethod.POST)
public Result<WxFacePayAuthinfoResp> getWxpayfaceCode(@RequestBody WxFaceOrderVo wxFaceOrderVo) throws Exception {
//1 初始化
this.initWxpayface();
//2 获取数据getWxpayfaceRawdata
WxFacePayResp wxFacePayResp = this.getWxpayfaceRawdata();
//String rawdata = wxFacePayResp.getRawdata();
//3、获取调用凭证get_wxpayface_authinfo(rawdata)(获取调用凭证)
WxFacePayAuthinfoResp wxFacePayResp0 = this.getWxpayfaceAuthinfo(wxFacePayResp.getRawdata());
//4、进行人脸识别getWxpayfaceCode获取支付凭证
String outTradeNo = getOutTradeNo();//获取流水号
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("getWxpayfaceCode", "1", System.currentTimeMillis() / 1000);
wxFacePayReq.setAuthinfo(wxFacePayResp0.getAuthinfo())
.setOut_trade_no(outTradeNo)//订单流水号
.setTotal_fee(wxFaceOrderVo.getTotalAmount())//金额:分
*/
/**
* 目标face_code类型可选值"1"刷卡付款码18位数字通过「付款码支付/被扫支付」接口完成支付,
* 如果不填写则默认为"0",人脸付款码:数字字母混合,通过「刷脸支付」接口完成支付。
*/
/*
.setFace_code_type("1");
WxFacePayAuthinfoResp authinfoResp = weChatPayFaceService.getWxpayfaceCode(wxFacePayReq);
System.out.println("**************4、进行人脸识别结果" + authinfoResp.toString());
if (StringUtil.isNotBlank(authinfoResp.getFace_code())) {
//5、调用后台人脸支付API发起支付
WxPayMicropayResult micropayResult = this.toCreateWxOrder(authinfoResp, wxFaceOrderVo, wxFacePayResp0);
//6、向后端查询订单状态
//WxPayOrderQueryResult queryResult = queryOrderByNo(micropayResult);
boolean isSuccess = pollOrderStatus(micropayResult);
if (isSuccess) {
}
}
return Result.ok(authinfoResp);
}
*/
}

View File

@@ -0,0 +1,174 @@
package com.dpkj.modules.scanface.wx.controller;
import com.dpkj.common.vo.Result;
import com.dpkj.modules.scanface.wx.dll.WxpayFaceSDKDll;
import com.dpkj.modules.scanface.wx.service.CallWxpayFaceService;
import com.dpkj.modules.scanface.wx.service.WeChatPayFaceService;
import com.dpkj.modules.scanface.wx.service.impl.CallWxpayFaceServiceImpl;
import com.dpkj.modules.scanface.wx.service.impl.WeChatPayFaceServiceImpl;
import com.dpkj.modules.scanface.wx.vo.WxFacePayAuthinfoResp;
import com.dpkj.modules.scanface.wx.vo.WxFacePayReq;
import com.dpkj.modules.scanface.wx.vo.WxFacePayResp;
import com.fasterxml.jackson.core.JsonProcessingException;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.thymeleaf.util.StringUtils;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.Provider;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
/**
* @description:
* @author: Zhangxue
* @time: 2024/11/28 16:43
*/
@Slf4j
@RestController
@RequestMapping("/wxpayFaceTest")
public class WxpayFaceTestController {
@Resource
private CallWxpayFaceService callWxpayFaceService;
@Resource
private WeChatPayFaceService weChatPayFaceService;
/**
* 1 程序启动时初始化 :程序启动时初始化initWxpayface
*
* @return
* @throws JsonProcessingException
*/
@RequestMapping(value = "/initWxpayface", method = RequestMethod.GET)
public Result<WxFacePayResp> initWxpayface() throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("initWxpayface", "1", System.currentTimeMillis() / 1000);
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
System.out.println("**************1、程序启动时初始化" + wxFacePayResp.toString());
return Result.ok(wxFacePayResp);
}
/**
* 2 获取数据getWxpayfaceRawdata
*
* @return
* @throws JsonProcessingException
*/
@RequestMapping(value = "/getWxpayfaceRawdata", method = RequestMethod.POST)
public Result<WxFacePayResp> getWxpayfaceRawdata() throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("getWxpayfaceRawdata", "1", System.currentTimeMillis() / 1000);
WxFacePayResp wxFacePayResp = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
System.out.println("**************2、获取数据" + wxFacePayResp.toString());
String rawdata = wxFacePayResp.getRawdata();
return Result.ok(wxFacePayResp);
}
/**
* 3、获取调用凭证get_wxpayface_authinfo(rawdata)(获取调用凭证)
*
* @return
* @throws Exception
*/
@RequestMapping(value = "/getWxpayfaceAuthinfo", method = RequestMethod.POST)
public Result<WxFacePayAuthinfoResp> getWxpayfaceAuthinfo() throws Exception {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("getWxpayfaceRawdata", "1", System.currentTimeMillis() / 1000);
WxFacePayResp wxFacePayResp1 = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq);
System.out.println("**************3、获取数据getWxpayfaceRawdata" + wxFacePayResp1.toString());
WxFacePayAuthinfoResp wxFacePayResp = new WxFacePayAuthinfoResp();
if ("SUCCESS".equals(wxFacePayResp1.getReturn_code())) {
String rawdata = wxFacePayResp1.getRawdata();
// 构建请求参数的JSON字符串
wxFacePayResp = weChatPayFaceService.getWxFaceAuthInfoReqMap(rawdata);
System.out.println("**************3、获取调用凭证get_wxpayface_authinfo" + wxFacePayResp.toString());
}
return Result.ok(wxFacePayResp);
}
/**
* 4、进行人脸识别getWxpayfaceCode获取支付凭证
*
* @return
* @throws Exception
*/
@RequestMapping(value = "/getWxpayfaceCode", method = RequestMethod.POST)
public Result<WxFacePayAuthinfoResp> getWxpayfaceCode() throws Exception {
WxFacePayAuthinfoResp wxFacePayResp0 = new WxFacePayAuthinfoResp();
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq0 = new WxFacePayReq("getWxpayfaceRawdata", "1", System.currentTimeMillis() / 1000);
WxFacePayResp wxFacePayResp1 = weChatPayFaceService.doWxPayIniMethod(wxFacePayReq0);
System.out.println("**************3、获取数据getWxpayfaceRawdata" + wxFacePayResp1.toString());
if ("SUCCESS".equals(wxFacePayResp1.getReturn_code())) {
String rawdata = wxFacePayResp1.getRawdata();
// 构建请求参数的JSON字符串
wxFacePayResp0 = weChatPayFaceService.getWxFaceAuthInfoReqMap(rawdata);
System.out.println("**************3、获取调用凭证get_wxpayface_authinfo" + wxFacePayResp0.toString());
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("getWxpayfaceCode", "1", System.currentTimeMillis() / 1000);
wxFacePayReq.setAuthinfo(wxFacePayResp0.getAuthinfo())
.setOut_trade_no("")//订单流水号
.setTotal_fee(new String("1"));
WxFacePayAuthinfoResp authinfoResp = weChatPayFaceService.getWxpayfaceCode(wxFacePayReq);
System.out.println("**************4、进行人脸识别结果" + authinfoResp.toString());
if (StringUtil.isNotBlank(authinfoResp.getFace_code())) {
//调用后台人脸支付API发起支付
authinfoResp.setOut_trade_no(wxFacePayReq.getOut_trade_no())//订单流水号
.setTotal_fee(wxFacePayReq.getTotal_fee());
}
return Result.ok(authinfoResp);
} else {
return Result.error("微信获取调用凭证失败");
}
}
//5、进行发起订单支付
//可以打印 JDK 中的 Provider 列表,以及所有签名算法
public void outProvider() {
TreeSet<String> algorithms = new TreeSet<>();
Provider[] providers = Security.getProviders();
System.out.println("-----Provider 列表如下:-----");
for (Provider provider : providers) {
System.out.println(provider.getName());
}
System.out.println("-----支持的签名算法如下:-----");
for (Provider provider : providers) {
for (Provider.Service service : provider.getServices())
if (service.getType().equals("Signature")) {
algorithms.add(service.getAlgorithm());
}
}
for (String algorithm : algorithms) {
System.out.println(algorithm);
}
}
}

View File

@@ -0,0 +1,71 @@
package com.dpkj.modules.scanface.wx.dll;
import com.sun.jna.Library;
import com.sun.jna.Native;
import lombok.extern.slf4j.Slf4j;
/**
* @description: 32位的WxpayFaceSDK /resources/win32-x86/WxpayFaceSDK.dll
* @author: Zhangxue
* @time: 2025/4/17 17:02
*/
@Slf4j
public class WxpayFaceSDKDll {
/**
* 获取 Dll 实例,同时注册 Dll 控件。
*
* @return WxpayFaceSDKDll 实例
* @throws DllRegistrationException 如果注册控件失败,抛出此异常
*/
public static Dll instance() throws DllRegistrationException {
try {
return Native.load("WxpayFaceSDK", Dll.class);
} catch (UnsatisfiedLinkError e) {
log.info("[WxpayFaceSDK][instance][微信扫脸动态库] SDK注册失败{}", e.getMessage());
throw new DllRegistrationException("Failed to load WxpayFaceSDK library: ", e);
}
}
/**
* 定义自定义异常类,用于表示注册控件时发生的错误
*/
public static class DllRegistrationException extends Exception {
public DllRegistrationException(String message) {
super(message);
}
public DllRegistrationException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 定义接口映射本地库中的函数。
*/
public interface Dll extends Library {
/**
* 调用人脸服务
* 注意这里的方法签名需要与DLL中的C函数签名一致
* req和resp都是JSON字符串需要转换为Pointer类型
* char * -->String; char ** -->String[]
* int -->int; int* --> IntByReference
* @param reqBuf 请求参数(JSON字符串)
* @param reqSize 请求参数长度
* @param pRespBuf 用来接收响应结果(JSON字符串)的char**指针
* @param pRespSize 用来接收响应结果长度的unsigned int*指针
* @return 如果成功返回0失败则返回非0
*/
//int wxpayCallFaceService(String reqBuf, int reqSize, String[] pRespBuf, IntByReference pRespSize);
int wxpayCallFaceService(String reqBuf, int reqSize, long[] pRespBuf, int[] pRespSize);
/**
* char ** -->String[]
* 释放人脸服务的响应字符串调用wxpayCallFaceService成功后务必调用此函数释放内存
* @param pRespBuf 指向响应结果(JSON字符串)的指针
*/
void wxpayReleaseResponse(String[] pRespBuf);
}
}

View File

@@ -0,0 +1,19 @@
package com.dpkj.modules.scanface.wx.service;
import com.dpkj.modules.scanface.wx.dll.WxpayFaceSDKDll;
import java.io.UnsupportedEncodingException;
import java.util.List;
public interface CallWxpayFaceService {
/**
* 调用SDK微信刷脸服务
* @param reqJson
* @param respJson
* @return
*/
String callWxpayFaceService(String reqJson, List<String> respJson) throws UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException;
}

View File

@@ -0,0 +1,54 @@
package com.dpkj.modules.scanface.wx.service;
import com.dpkj.modules.scanface.wx.dll.WxpayFaceSDKDll;
import com.dpkj.modules.scanface.wx.vo.WxFacePayAuthinfoResp;
import com.dpkj.modules.scanface.wx.vo.WxFacePayReq;
import com.dpkj.modules.scanface.wx.vo.WxFacePayResp;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
/**
* @description: 调用微信刷脸方法
* @author: Zhangxue
* @time: 2024/12/6 10:45
*/
public interface WeChatPayFaceService {
/**
* 调用方法
* 1、程序启动时初始化initWxpayface2、获取数据getWxpayfaceRawdata
* 8、更新支付结果updateWxpayfacePayResult
* 9、停止刷脸支付stopWxpayface
* @param wxFacePayReq
* @return
* @throws UnsupportedEncodingException
* @throws JsonProcessingException
*/
WxFacePayResp doWxPayIniMethod(WxFacePayReq wxFacePayReq) throws UnsupportedEncodingException, JsonProcessingException, WxpayFaceSDKDll.DllRegistrationException;
/**
* @description: 3、获取调用凭证get_wxpayface_authinfo(rawdata)
* @author: zhangxue
* @date: 2024/12/4 14:35
* @Param rawData 初始化数据。由微信人脸SDK的接口返回。
* @return: java.util.Map<java.lang.String,java.lang.String>
*/
WxFacePayAuthinfoResp getWxFaceAuthInfoReqMap(String rawData) throws Exception;
/**
* 调用方法
* 4、进行人脸识别getWxpayfaceCode获取支付凭证
* @param wxFacePayReq
* @return
* @throws UnsupportedEncodingException
* @throws JsonProcessingException
*/
WxFacePayAuthinfoResp getWxpayfaceCode(WxFacePayReq wxFacePayReq) throws UnsupportedEncodingException, JsonProcessingException, WxpayFaceSDKDll.DllRegistrationException;
}

View File

@@ -0,0 +1,79 @@
package com.dpkj.modules.scanface.wx.service.impl;
import com.dpkj.modules.scanface.wx.config.WxMpProperties;
import com.dpkj.modules.scanface.wx.dll.WxpayFaceSDKDll;
import com.dpkj.modules.scanface.wx.service.CallWxpayFaceService;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.List;
/**
* @description:
* @author: Zhangxue
* @time: 2024/11/28 10:04
*/
@Slf4j
@Service
public class CallWxpayFaceServiceImpl implements CallWxpayFaceService {
//将 BouncyCastleProvider 添加到 Provider 列表中
static {
Security.addProvider(new BouncyCastleProvider());
}
//赋值
@Autowired
private WxMpProperties wxMpProperties;
/**
* 调用SDK微信刷脸服务
* @param reqJson
* @param respJson
* @return
*/
@Override
public String callWxpayFaceService(String reqJson, List<String> respJson) throws WxpayFaceSDKDll.DllRegistrationException {
//组装请求数据
Pointer reqPointer = new Memory(reqJson.length() + 1);
reqPointer.setString(0, reqJson);
//请求数据长度
int reqSize = reqPointer.getString(0).length();
System.out.println("-----------调用微信刷脸DLL请求数据----------"+reqPointer.getString(0));
//接收响应
long[] pRespBuf = new long[1];
int[] respSize = new int[1];
String resStr = new String();
// 调用本地方法
WxpayFaceSDKDll.Dll dll = WxpayFaceSDKDll.instance();
int result = dll.wxpayCallFaceService(reqPointer.getString(0), reqSize, pRespBuf, respSize);
//使用C:\Windows\System32目录下 int result = WxpayFaceSDK.INSTANCE.wxpayCallFaceService(reqPointer.getString(0), reqSize, pRespBuf, respSize);
if (result == 0) {
Pointer pointer = new Pointer(pRespBuf[0]);
byte[] byteArray = pointer.getByteArray(0, respSize[0]);
resStr = new String(byteArray, StandardCharsets.UTF_8);
System.out.println("-----------调用微信刷脸DLL请求结果: " + resStr);
dll.wxpayReleaseResponse(new String[2]);
//释放 C:\Windows\System32目录下 WxpayFaceSDK.INSTANCE.wxpayReleaseResponse(new String[2]);
} else {
Pointer pointer = new Pointer(pRespBuf[0]);
byte[] byteArray = pointer.getByteArray(0, respSize[0]);
resStr = new String(byteArray, StandardCharsets.UTF_8);
System.err.println("-----------调用人脸服务失败: " +resStr);
}
return resStr;
}
}

View File

@@ -0,0 +1,406 @@
package com.dpkj.modules.scanface.wx.service.impl;
import com.dpkj.common.constant.WxConstant;
import com.dpkj.modules.scanface.wx.config.WechatUrlConfig;
import com.dpkj.modules.scanface.wx.config.WxMpProperties;
import com.dpkj.modules.scanface.wx.dll.WxpayFaceSDKDll;
import com.dpkj.modules.scanface.wx.service.CallWxpayFaceService;
import com.dpkj.modules.scanface.wx.service.WeChatPayFaceService;
import com.dpkj.modules.scanface.wx.util.WXPayUtil;
import com.dpkj.modules.scanface.wx.util.WxRandomUtils;
import com.dpkj.modules.scanface.wx.util.XmlParserUtil;
import com.dpkj.modules.scanface.wx.util.XmlUtils;
import com.dpkj.modules.scanface.wx.vo.WxFacePayAuthinfoResp;
import com.dpkj.modules.scanface.wx.vo.WxFacePayMicroPayResp;
import com.dpkj.modules.scanface.wx.vo.WxFacePayOrderResp;
import com.dpkj.modules.scanface.wx.vo.WxFacePayReq;
import com.dpkj.modules.scanface.wx.vo.WxFacePayResp;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @description: 调用微信刷脸方法
* @author: Zhangxue
* @time: 2024/12/6 10:45
*/
@Slf4j
@Service
public class WeChatPayFaceServiceImpl implements WeChatPayFaceService {
@Autowired
private CallWxpayFaceService callWxpayFaceService;
//赋值
@Autowired
private WxMpProperties wxMpProperties;
/**
* 调用方法
* 1、程序启动时初始化initWxpayface2、获取数据getWxpayfaceRawdata
*
* @param wxFacePayReq
* @return
* @throws UnsupportedEncodingException
* @throws JsonProcessingException
*/
@Override
public WxFacePayResp doWxPayIniMethod(WxFacePayReq wxFacePayReq) throws JsonProcessingException, WxpayFaceSDKDll.DllRegistrationException {
// 构建请求参数的JSON字符串
ObjectMapper mapper = new ObjectMapper();
String reqJson = mapper.writeValueAsString(wxFacePayReq);
// 创建一个Pointer来接收响应
//List<String> pResp= new ArrayList<>();
//String result = callWxpayFaceService.callWxpayFaceService(req,pResp);
//组装请求数据
Pointer reqPointer = new Memory(reqJson.length() + 1);
reqPointer.setString(0, reqJson);
//请求数据长度
int reqSize = reqPointer.getString(0).length();
System.out.println("-----------调用微信刷脸DLL请求数据----------" + reqPointer.getString(0));
//接收响应
long[] pRespBuf = new long[1];
int[] respSize = new int[1];
String resStr = new String();
// 调用本地方法
WxpayFaceSDKDll.Dll dll = WxpayFaceSDKDll.instance();
int result = dll.wxpayCallFaceService(reqPointer.getString(0), reqSize, pRespBuf, respSize);
//使用C:\Windows\System32目录下 int result = WxpayFaceSDK.INSTANCE.wxpayCallFaceService(reqPointer.getString(0), reqSize, pRespBuf, respSize);
log.info("[WeChatPayFaceServiceImpl][doWxPayIniMethod][95] DLL 返回码: {}", result);
if (result == 0) {
Pointer pointer = new Pointer(pRespBuf[0]);
byte[] byteArray = pointer.getByteArray(0, respSize[0]);
resStr = new String(byteArray, StandardCharsets.UTF_8);
System.out.println("-----------微信刷脸DLL调用服务成功结果: " + resStr);
//释放
dll.wxpayReleaseResponse(new String[2]);
//释放 C:\Windows\System32目录下 WxpayFaceSDK.INSTANCE.wxpayReleaseResponse(new String[2]);
} else {
Pointer pointer = new Pointer(pRespBuf[0]);
byte[] byteArray = pointer.getByteArray(0, respSize[0]);
resStr = new String(byteArray, StandardCharsets.UTF_8);
System.err.println("-----------调用人脸服务失败: " + resStr);
}
//响应结果
WxFacePayResp wxFacePayResp = mapper.readValue(resStr, WxFacePayResp.class);
return wxFacePayResp;
}
/**
* @description: 3、获取调用凭证get_wxpayface_authinfo(rawdata)
* @author: zhangxue
* @date: 2024/12/4 14:35
* @Param rawData 初始化数据。由微信人脸SDK的接口返回。
* @return: java.util.Map<java.lang.String, java.lang.String>
*/
@Override
public WxFacePayAuthinfoResp getWxFaceAuthInfoReqMap(String rawData) throws Exception {
try {
SortedMap<String, String> map = new TreeMap<String, String>();
//门店编号, 由商户定义, 各门店唯一。
map.put("store_id", WxConstant.STORE_ID);
//门店名称,由商户定义。(可用于展示);中文会导致调用失败
String text = WxConstant.STORE_TEXT;
String storeName = Base64.getEncoder().encodeToString(text.getBytes());
map.put("store_name", storeName);
//终端设备编号,由商户定义
map.put("device_id", WxConstant.DEVICE_ID);
//初始化数据。由微信人脸SDK的接口返回。
map.put("rawdata", rawData);
//商户号绑定的公众号/小程序 appid
map.put("appid", wxMpProperties.getMchConfig().getAppId());
//商户号
map.put("mch_id", wxMpProperties.getMchConfig().getMchId());
//取当前时间10位unix时间戳。
long timeStampSec = System.currentTimeMillis() / 1000;
String timestamp = String.format("%010d", timeStampSec);
map.put("now", timestamp);
//版本号。固定为1
map.put("version", WxConstant.VERSION);
//随机字符串不长于32位:工具类微信随机数
map.put("nonce_str", WxRandomUtils.getNonceStr());
//参数签名,使用MD5
map.put("sign_type", WxConstant.SING_TYPE);
//加密和生成微信v2指定的xml格式
// WXPayUtil.createSign("UTF-8",map,wxMpProperties.getMchConfig().getKeyApi());
String sign = WXPayUtil.generateSignedXml(map, wxMpProperties.getMchConfig().getKeyApi(), WxPayConstants.SignType.MD5);
log.info("--------3、获取调用凭证 构建微信获取刷脸授权XML参数{}", sign);
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> stringHttpEntity = new HttpEntity<String>(sign, headers);
RestTemplate restTemplate = new RestTemplate();
//发起http调用
ResponseEntity<String> exchange = restTemplate.exchange(WechatUrlConfig.GET_WXPAYFACE_AUTHINFO,
HttpMethod.POST,
stringHttpEntity,
String.class);
log.info("--------3、获取调用凭证 发起http调用结果{}", exchange.getBody());
XmlParserUtil.extractAuthInfo(exchange.getBody());
//转成map方便取值
Map<String, String> stringMap = XmlUtils.xmlParser(exchange.getBody(), "xml");
log.info("------3、获取调用凭证 调用结果转换为map{}", stringMap);
//转成返回对象
WxFacePayAuthinfoResp wxFacePayAuthinfoResp = XmlUtils.mapToObject(
stringMap,
WxFacePayAuthinfoResp.class
);
log.info("--------发起http调用结果转换为WxFacePayAuthinfoResp", wxFacePayAuthinfoResp.toString());
return wxFacePayAuthinfoResp;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 调用方法
* 4、进行人脸识别getWxpayfaceCode获取支付凭证
*
* @param wxFacePayReq
* @return
* @throws UnsupportedEncodingException
* @throws JsonProcessingException
*/
@Override
public WxFacePayAuthinfoResp getWxpayfaceCode(WxFacePayReq wxFacePayReq) throws JsonProcessingException, UnsupportedEncodingException, WxpayFaceSDKDll.DllRegistrationException {
//设置参数
wxFacePayReq.setAppid(wxMpProperties.getMchConfig().getAppId())
.setMch_id(wxMpProperties.getMchConfig().getMchId())
.setStore_id(WxConstant.STORE_ID)
.setFace_authtype(WxConstant.FACEPAY)
.setAuthinfo(wxFacePayReq.getAuthinfo());
// 构建请求参数的JSON字符串
ObjectMapper mapper = new ObjectMapper();
String req = mapper.writeValueAsString(wxFacePayReq);
// 创建一个Pointer来接收响应
List<String> pResp = new ArrayList<>();
String result = callWxpayFaceService.callWxpayFaceService(req, pResp);
System.out.println("4、进行人脸识别getWxpayfaceCode结果"+result);
log.info("[WeChatPayFaceServiceImpl][getWxpayfaceCode][220][4、进行人脸识别getWxpayfaceCode结果] {}", result);
//响应结果
WxFacePayAuthinfoResp authinfoResp = mapper.readValue(result, WxFacePayAuthinfoResp.class);
return authinfoResp;
}
//5、进行发起订单支付
public Map<String, String> createWxOrder() throws Exception {
SortedMap<String, String> map = new TreeMap<String, String>();
//微信分配的公众账号ID企业号corpid即为此appId
map.put("appid", wxMpProperties.getMchConfig().getAppId());
//商户号
map.put("mch_id", wxMpProperties.getMchConfig().getMchId());
//随机字符串不长于32位:工具类微信随机数
map.put("nonce_str", WxRandomUtils.getNonceStr());
//签名, map.put("sign", "");
//商品简单描述,
map.put("body", "");
//商户系统内部订单号要求32个字符内只能是数字、大小写字母_-|*且在同一个商户号下唯一
map.put("out_trade_no", "");
//Int 订单总金额,单位为分,只能为整数
map.put("total_fee", "");
//支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
map.put("spbill_create_ip", "");
//扫码支付付款码,设备读取用户微信中的条码或者二维码信息
map.put("auth_code", "");
//加密和生成微信v2指定的xml格式
// WXPayUtil.createSign("UTF-8",map,wxMpProperties.getMchConfig().getKeyApi());
String sign = WXPayUtil.generateSignedXml(map, wxMpProperties.getMchConfig().getKeyApi(), WxPayConstants.SignType.MD5);
log.info("--------构建微信获取刷脸授权XML参数【{}】", sign);
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> stringHttpEntity = new HttpEntity<String>(sign, headers);
RestTemplate restTemplate = new RestTemplate();
//发起http调用
ResponseEntity<String> exchange = restTemplate.exchange(WechatUrlConfig.CREATEORDER,
HttpMethod.POST,
stringHttpEntity,
String.class);
log.info("--------5、进行发起订单支付:发起http调用结果【{}】", exchange.getBody());
//转成map方便取值
Map<String, String> stringMap = XmlUtils.xmlParser(exchange.getBody(), "xml");
//转成返回对象
ObjectMapper mapper = new ObjectMapper();
WxFacePayMicroPayResp microPayResp = mapper.readValue(exchange.getBody(), WxFacePayMicroPayResp.class);
log.info("--------5、进行发起订单支付,发起http调用结果转换", microPayResp.toString());
return stringMap;
}
//6、查询订单状态
public Map<String, String> orderquery() throws Exception {
SortedMap<String, String> map = new TreeMap<String, String>();
//微信支付分配的公众账号ID企业号corpid即为此appId
map.put("appid", wxMpProperties.getMchConfig().getAppId());
//微信支付分配的商户号
map.put("mch_id", wxMpProperties.getMchConfig().getMchId());
//微信的订单号,建议优先使用;必填:否
map.put("transaction_id", "");
//商户系统内部订单号;必填:否
map.put("out_trade_no", "");
//随机字符串不长于32位:工具类微信随机数
map.put("nonce_str", WxRandomUtils.getNonceStr());
//参数签名,使用MD5
map.put("sign_type", "MD5");
//sign:通过签名算法计算得出的签名值
String sign = WXPayUtil.generateSignedXml(map, wxMpProperties.getMchConfig().getKeyApi(), WxPayConstants.SignType.MD5);
log.info("--------构建微信获取刷脸授权XML参数【{}】", sign);
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> stringHttpEntity = new HttpEntity<String>("", headers);
RestTemplate restTemplate = new RestTemplate();
//发起http调用
ResponseEntity<String> exchange = restTemplate.exchange(WechatUrlConfig.ORDERQUERY,
HttpMethod.POST,
stringHttpEntity,
String.class);
log.info("--------6、查询订单状态,发起http调用结果【{}】", exchange.getBody());
//转成map方便取值
Map<String, String> stringMap = XmlUtils.xmlParser(exchange.getBody(), "xml");
//转成返回对象
ObjectMapper mapper = new ObjectMapper();
WxFacePayOrderResp orderResp = mapper.readValue(exchange.getBody(), WxFacePayOrderResp.class);
log.info("--------6、查询订单状态,发起http调用结果转换", orderResp.toString());
return stringMap;
}
//7、撤销交易
public Map<String, String> reverse() throws Exception {
SortedMap<String, String> map = new TreeMap<String, String>();
//微信分配的公众账号ID企业号corpid即为此appId
map.put("appid", wxMpProperties.getMchConfig().getAppId());
//商户号
map.put("mch_id", wxMpProperties.getMchConfig().getMchId());
//微信的订单号,优先使用
map.put("transaction_id", "");
//商户系统内部的订单号,transaction_id、out_trade_no二选一如果同时存在优先级transaction_id> out_trade_no
map.put("out_trade_no", "");
//随机字符串不长于32位。
map.put("nonce_str", WxRandomUtils.getNonceStr());
//加密和生成微信v2指定的xml格式
String sign = WXPayUtil.generateSignedXml(map, wxMpProperties.getMchConfig().getKeyApi(), WxPayConstants.SignType.MD5);
log.info("--------构建微信XML参数【{}】", sign);
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> stringHttpEntity = new HttpEntity<String>("", headers);
RestTemplate restTemplate = new RestTemplate();
//发起http调用
ResponseEntity<String> exchange = restTemplate.exchange(WechatUrlConfig.REVERSE,
HttpMethod.POST,
stringHttpEntity,
String.class);
log.info("--------7、撤销交易,发起http调用结果【{}】", exchange.getBody());
//转成map方便取值
Map<String, String> stringMap = XmlUtils.xmlParser(exchange.getBody(), "xml");
//转成返回对象
ObjectMapper mapper = new ObjectMapper();
WxFacePayOrderResp orderResp = mapper.readValue(exchange.getBody(), WxFacePayOrderResp.class);
log.info("--------7、撤销交易,发起http调用结果转换", orderResp.toString());
return stringMap;
}
//8、更新支付结果通知人脸SDK更新支付结果
public WxFacePayResp updateWxpayfacePayResult(String storeId, String authInfo, String payresult) throws Exception {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("updateWxpayfacePayResult", "1", System.currentTimeMillis() / 1000);
//微信分配的公众账号ID企业号corpid即为此appId
wxFacePayReq.setAppid(wxMpProperties.getMchConfig().getAppId());
//商户号
wxFacePayReq.setMch_id(wxMpProperties.getMchConfig().getMchId());
//门店编号
wxFacePayReq.setStore_id(storeId);
//调用凭证
wxFacePayReq.setAuthinfo(authInfo);
//支付结果。可取值SUCCESS: 支付成功、ERROR: 支付失败
wxFacePayReq.setPayresult(payresult);
// 构建请求参数的JSON字符串
ObjectMapper mapper = new ObjectMapper();
String req = mapper.writeValueAsString(wxFacePayReq);
// 创建一个Pointer来接收响应
List<String> pResp = new ArrayList<>();
String result = callWxpayFaceService.callWxpayFaceService(req, pResp);
//响应结果
WxFacePayResp authinfoResp = mapper.readValue(result, WxFacePayResp.class);
return authinfoResp;
}
//9、停止刷脸支付
public WxFacePayResp stopWxpayface(String storeId, String authInfo, String payresult) throws Exception {
// 构建请求参数的JSON字符串
WxFacePayReq wxFacePayReq = new WxFacePayReq("stopWxpayface", "1", System.currentTimeMillis() / 1000);
//微信分配的公众账号ID企业号corpid即为此appId
wxFacePayReq.setAppid(wxMpProperties.getMchConfig().getAppId());
//商户号
wxFacePayReq.setMch_id(wxMpProperties.getMchConfig().getMchId());
//调用凭证
wxFacePayReq.setAuthinfo(authInfo);
// 构建请求参数的JSON字符串
ObjectMapper mapper = new ObjectMapper();
String req = mapper.writeValueAsString(wxFacePayReq);
// 创建一个Pointer来接收响应
List<String> pResp = new ArrayList<>();
String result = callWxpayFaceService.callWxpayFaceService(req, pResp);
//响应结果
WxFacePayResp authinfoResp = mapper.readValue(result, WxFacePayResp.class);
return authinfoResp;
}
}

View File

@@ -0,0 +1,48 @@
package com.dpkj.modules.scanface.wx.util;
import java.security.MessageDigest;
/**
* @description: MD5工具类
* @author: Zhangxue
* @time: 2024/12/5 11:34
*/
public class MD5Util {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}

View File

@@ -0,0 +1,115 @@
package com.dpkj.modules.scanface.wx.util;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import javax.xml.bind.DatatypeConverter;
import java.io.StringReader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @description: 生成签名
* @author: Zhangxue
* @time: 2024/12/4 14:27
*/
public class WXPayUtil {
/**
* 生成微信API请求的MD5签名并返回XML格式的字符串
*
* @param params 参数Map
* @param apiKey 微信API密钥
* @return 微信v2指定的XML格式字符串
*/
public static String generateSignedXml(Map<String, String> params, String apiKey,String signType) {
// 将参数Map按照key的字典顺序排序
Map<String, String> sortedParams = new TreeMap<>(params);
// 构建签名原文
StringBuilder signSrc = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (!"sign".equals(entry.getKey())) {
signSrc.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
signSrc.append("key=").append(apiKey);
// 计算MD5签名
String sign = getMD5(signSrc.toString(),signType).toUpperCase();
// 构建XML格式的字符串
StringBuilder xmlBuilder = new StringBuilder();
xmlBuilder.append("<xml>");
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
xmlBuilder.append("<").append(entry.getKey()).append(">").append(entry.getValue()).append("</").append(entry.getKey()).append(">");
}
xmlBuilder.append("<sign><![CDATA[").append(sign).append("]]></sign>");
xmlBuilder.append("</xml>");
return xmlBuilder.toString();
}
/**
* 使用MD5算法计算字符串的签名
*
* @param input 输入字符串
* @return MD5签名字符串
*/
private static String getMD5(String input,String signType) {
try {
MessageDigest md = MessageDigest.getInstance(signType);
byte[] digest = md.digest(input.getBytes());
return DatatypeConverter.printHexBinary(digest).toLowerCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5算法不支持", e);
}
}
public static Map<String, String> xmlToMap(String xml) throws Exception {
Map<String, String> map = new HashMap<>();
Document document = new Document((List<? extends Content>) new StringReader(xml));
Element root = document.getRootElement();
for (Element child : root.getChildren()) {
map.put(child.getName(), child.getText());
}
return map;
}
//定义签名微信根据参数字段的ASCII码值进行排序 加密签名,故使用SortMap进行参数排序
public static String createSign(String characterEncoding, SortedMap<String,String> parameters,String key){
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);//最后加密时添加商户密钥由于key值放在最后所以不用添加到SortMap里面去单独处理编码方式采用UTF-8
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
}

View File

@@ -0,0 +1,38 @@
package com.dpkj.modules.scanface.wx.util;
import java.security.SecureRandom;
import java.util.Random;
public class WxRandomUtils {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String getNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* 拼接参数
*
* @return
*/
private static String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
return appId + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ packag + "\n";
}
}

View File

@@ -0,0 +1,45 @@
package com.dpkj.modules.scanface.wx.util;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayInputStream;
/**
* @description: 解析微信返回的xml数据
* @author: Zhangxue
* @time: 2025/5/28 10:10
*/
public class XmlParserUtil {
/**
* 解析微信返回的xml数据获取到authinfo
* @param xmlResponse
* @return
* @throws Exception
*/
public static String extractAuthInfo(String xmlResponse) throws Exception {
// 1. 创建DocumentBuilder解析XML
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 2. 将字符串转为输入流
ByteArrayInputStream input = new ByteArrayInputStream(xmlResponse.getBytes("UTF-8"));
Document doc = builder.parse(input);
// 3. 创建XPath表达式定位<authinfo>节点
XPath xpath = XPathFactory.newInstance().newXPath();
String expression = "//authinfo"; // 使用XPath查找所有authinfo节点
// 4. 提取节点文本内容自动处理CDATA
Node authInfoNode = (Node) xpath.evaluate(expression, doc, XPathConstants.NODE);
if (authInfoNode != null) {
return authInfoNode.getTextContent();
} else {
throw new RuntimeException("未找到<authinfo>节点");
}
}
}

View File

@@ -0,0 +1,114 @@
package com.dpkj.modules.scanface.wx.util;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* @description: xml解析为Map
* @author: Zhangxue
* @time: 2024/12/5 11:42
*/
public class XmlUtils {
/**
* xml解析器
*
* @param xml xml字符串
* @param filterRootEleName 匹配的根节点名
* @return
* @返回map格式
* @返回属性值:->节点名=>属性名:属性值
* @返回节点值:->节点名:值
* @返回子节点属性值:->节点名->子节点名=>属性名:属性值
* @返回子节点值:->节点名->子节点名:属性值
*/
public static Map<String, String> xmlParser(String xml, String filterRootEleName) {
Map<String, String> retMap = new HashMap<>();
//1.创建Reader对象
try {
SAXReader reader = new SAXReader();
InputStream targetStream = IOUtils.toInputStream(xml, StandardCharsets.UTF_8.name());
Document document = reader.read(targetStream);
//3.获取根节点
Element rootElement = document.getRootElement();
StringBuilder builder = new StringBuilder();
parser(rootElement, builder, filterRootEleName, retMap);
} catch (Exception ex) {
ex.printStackTrace();
}
return retMap;
}
/**
* xml递归解析器
*
* @param ele 解析节点
* @param eleKey 上级节点key
* @param retMap 返回map
*/
private static void parser(Element ele, StringBuilder eleKey, String firstEleName, Map<String, String> retMap) {
StringBuilder builder = new StringBuilder();//eleKey.toString()
if (StringUtils.isEmpty(firstEleName)
|| firstEleName.equals(ele.getName())) {
firstEleName = null;
//builder.append("->" + ele.getName());
if (StringUtils.isNotEmpty(StringUtils.stripToEmpty(ele.getData() + ""))) {
retMap.put(ele.getName(), StringUtils.stripToEmpty(ele.getData() + ""));
}
List<Attribute> attributes = ele.attributes();
for (Attribute attribute : attributes) {
StringBuilder builder1 = new StringBuilder(builder.toString());
builder1.append("=>" + attribute.getName());
if (StringUtils.isNotEmpty(attribute.getValue())) {
retMap.put(builder1.toString(), attribute.getValue());
}
}
}
Iterator iterator1 = ele.elementIterator();
while (iterator1.hasNext()) {
Element eleChild = (Element) iterator1.next();
parser(eleChild, builder, firstEleName, retMap);
}
}
/**
* 通用方法将Map转换为Java对象
*/
public static <T> T mapToObject(Map<String, String> map, Class<T> clazz) throws Exception {
T obj = clazz.getDeclaredConstructor().newInstance();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
String value = map.get(fieldName);
if (value != null) {
Class<?> type = field.getType();
// 类型转换逻辑
if (type == int.class || type == Integer.class) {
field.set(obj, Integer.parseInt(value));
} else if (type == boolean.class || type == Boolean.class) {
field.set(obj, Boolean.parseBoolean(value));
} else {
field.set(obj, value);
}
}
}
return obj;
}
}

View File

@@ -0,0 +1,20 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Map;
/**
* @description: 硬件提供参数
* @author: Zhangxue
* @time: 2024/11/28 10:01
*/
@Data
@Accessors(chain = true)
public class WxFaceAuthInfoReq {
private String deviceId;
private Map<String,String> rawData;
}

View File

@@ -0,0 +1,43 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @description: 微信刷脸订单参数
* @author: Zhangxue
* @time: 2025/5/28 15:35
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class WxFaceOrderVo {
/**
* 付款模块
*/
private String eventModule;
/**
* 患者Id
*/
private String patientId;
/**
* 商户机具终端编号
*/
private String terminalId;
/**
* 系统订单编号
*/
private String outTradeNo;
/**
* 用户支付金额 分
*/
private String totalAmount;
}

View File

@@ -0,0 +1,167 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @description: 3、获取调用凭证返回值
* @author: Zhangxue
* @time: 2024/12/6 9:23
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class WxFacePayAuthinfoResp {
/**
* 错误码
*/
private String return_code;
/**
* 对错误码的描述
*/
private String return_msg;
/**
* 版本号
*/
private String version;
/**
* 初始化数据。用于接口调用获取authinfo
*/
private String rawdata;
/**
* 3、获取调用凭证
* 必填:是
* SDK调用凭证。用于调用SDK的人脸识别接口。
*/
private String authinfo;
/**
* 3、获取调用凭证
* 必填:否
* authinfo的有效时间, 单位秒。
* 在有效时间内, 对于同一台终端设备,相同的参数的前提下(如:相同的公众号、商户号、 门店编号等可以用同一个authinfo多次调用SDK的getWxpayfaceCode接口。
*/
private int expires_in;
/**
* 3、获取调用凭证
* 必填:是
* 随机字符串
*/
private String nonce_str;
/**
* 3、获取调用凭证
* 必填:是
* 响应结果签名
*/
private String sign;
/**
* 3、获取调用凭证
* 必填:是
* 公众号
*/
private String appid;
/**
* 3、获取调用凭证
* 必填:否
* 商户号
*/
private String mch_id;
/**
* 3、获取调用凭证
* 必填:否
* 子商户公众账号ID(服务商模式)
*/
private String sub_appid;
/**
* 3、获取调用凭证
* 必填:是
* 子商户号(服务商模式)
*/
private String sub_mch_id;
/**
* 4、进行人脸识别
* 是否必填S
* 人脸凭证, 用于刷脸支付。
*/
private String face_code;
/**
* 4、进行人脸识别
* 是否必填S
* openid
*/
private String openid;
/**
* 4、进行人脸识别
* 是否必填:否
* 子商户号下的openid(服务商模式)
*/
private String sub_openid;
/**
* 4、进行人脸识别
* 是否必填:否
* 用户身份信息查询凭证
*/
private String face_sid;
/**
* 传参到后台发起微信订单方法
* 4、进行人脸识别
* 必填:否
* 订单金额(数字), 单位分该字段在在face_code_type为"1"时可不填,为"0"时必填
*/
private String total_fee;
/**
* 传参到后台发起微信订单方法
* 4、进行人脸识别
* 必填:否
* 商户订单号须与调用支付接口时字段一致该字段在在face_code_type为"1"时可不填,为"0"时必填
*/
private String out_trade_no;
/**
* 传参到后台发起微信订单方法
* 商品描述
*/
private String remark;
/**
* 传参到后台发起微信订单方法
* 患者Id
*/
private String patientId;
/**
* 传参到后台发起微信订单方法
* 付款模块
*/
private String eventModule;
/**
* 传参到后台发起微信订单方法
* 商户机具终端编号
*/
private String terminalId;
}

View File

@@ -0,0 +1,95 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @description: 5、进行发起订单支付 返回值
* @author: Zhangxue
* @time: 2024/12/6 9:23
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class WxFacePayMicroPayResp extends WxFacePayOrderResp{
/************当return_code 和result_code都为SUCCESS的时还会包括以下字段*******************/
/**
* 用户在商户appid 下的唯一标识
*/
private String openid;
/**
* 用户是否关注公众账号仅在公众账号类型支付有效取值范围Y或N;Y-关注;N-未关注
*/
private String is_subscribe;
/**
* MICROPAY 付款码支付
*/
private String trade_type;
/**
* 银行类型,采用字符串类型的银行标识
*/
private String bank_type;
/**
* 符合ISO 4217标准的三位字母代码默认人民币CNY
*/
private String fee_type;
/**
* 订单总金额,单位为分,只能为整数
*/
private Integer total_fee;
/**
* 当订单使用了免充值型优惠券后返回该参数,应结订单金额=订单金额-免充值优惠券金额。
*/
private Integer settlement_total_fee;
/**
* “代金券”金额<=订单金额,订单金额-“代金券”金额=现金支付金额,
*/
private Integer coupon_fee;
/**
* 符合ISO 4217标准的三位字母代码默认人民币CNY
*/
private String cash_fee_type;
/**
* 订单现金支付金额,
*/
private Integer cash_fee;
/**
* 微信支付订单号
*/
private String transaction_id;
/**
* 商户系统内部订单号要求32个字符内只能是数字、大小写字母_-|*且在同一个商户号下唯一。
*/
private String out_trade_no;
/**
* 商家数据包,原样返回
*/
private String attach;
/**
* 订单生成时间格式为yyyyMMddHHmmss
*/
private String time_end;
/**
* 新增返回,单品优惠功能字段
*/
private String promotion_detail;
}

View File

@@ -0,0 +1,136 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @description: 5、进行发起订单支付 返回值;6、查询订单状态
* @author: Zhangxue
* @time: 2024/12/10 15:30
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class WxFacePayOrderResp {
/**
* 错误码
* 文档https://pay.weixin.qq.com/wiki/doc/wxfacepay/develop/windows/facepay.html#_5%E3%80%81%E8%BF%9B%E8%A1%8C%E5%8F%91%E8%B5%B7%E8%AE%A2%E5%8D%95%E6%94%AF%E4%BB%98
*/
private String return_code;
/**
* 对错误码的描述
*/
private String return_msg;
/**
* 调用接口提交的公众账号ID
*/
private String appid;
/**
* 调用接口提交的商户号
*/
private String mch_id;
/**
* 调用接口提交的终端设备号,
*/
private String device_info;
/**
* 微信返回的随机字符串,
*/
private String nonce_str;
/**
* 微信返回的签名,
*/
private String sign;
/**
* SUCCESS/FAIL
*/
private String result_code;
/**
* 详细参见错误列表
*/
private String err_code;
/**
* 错误返回的信息描述
*/
private String err_code_des;
/**
* SUCCESS—支付成功,
* REFUND—转入退款当result_code为FAIL时返回错误代码
* NOTPAY—未支付当result_code为FAIL时返回错误描述
* CLOSED—已关闭REVOKED—已撤销付款码支付
* USERPAYING--用户支付中(付款码支付),
* PAYERROR--支付失败(其他原因,如银行返回失败)
*/
private String trade_state;
/**
* 商户系统内部订单号要求32个字符内只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
*/
private String out_trade_no;
/**
* 附加数据,原样返回
*/
private String attach;
/**
* 用户在商户appid下的唯一标识
*/
private String openid;
/**
* 用户是否关注公众账号Y-关注N-未关注
*/
private String is_subscribe;
/**
* 调用接口提交的交易类型取值如下JSAPINATIVEAPPMICROPAY
*/
private String trade_type;
/**
* 银行类型,采用字符串类型的银行标识
*/
private String bank_type;
/**
* 订单总金额,单位为分
*/
private Integer total_fee;
/**
* 现金支付金额订单现金支付金额
*/
private Integer cash_fee;
/**
* 微信支付订单号
*/
private String transaction_id;
/**
* 订单支付时间格式为yyyyMMddHHmmss如20091225091010
*/
private String time_end;
/**
* 对当前查询订单状态的描述和下一步操作的指引
*/
private int trade_state_desc;
}

View File

@@ -0,0 +1,169 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @description:WxFacePayReq
* @author: Zhangxue
* @time: 2024/11/29 17:18
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class WxFacePayReq {
/**
* 命令字,即各接口名:
* initWxpayface
* getWxpayfaceRawdata
* getWxpayfaceCode
* releaseWxpayface
*/
private String cmd;
/**
* 版本号, 固定填写1
*/
private String version;
/**
* 取当前时间的10位unix时间戮
*/
private long now;
/**
* 对摄像头画面进行旋转1为+90度2为180度3为-90度
*/
private int camera_rotation;
/**
* 4、进行人脸识别
* 必填:是
* 商户号绑定的公众号/小程序 appid
*/
private String appid;
/**
* 4、进行人脸识别
* 必填:是
* 商户号
*/
private String mch_id;
/**
* 4、进行人脸识别
* 必填:否
* 子商户绑定的公众号/小程序 appid(可不填)
*/
//private String sub_appid;
/**
* 4、进行人脸识别
* 必填:否
* 子商户号(非服务商模式不填)
*/
//private String sub_mch_id;
/**
* 4、进行人脸识别
* 必填:是
* 门店编号
*/
private String store_id;
/**
* 4、进行人脸识别
* 必填:否
* 通过getWxpayfaceUserInfo获取的openid传入后可使用快捷支付模式。1.24版本以上支持该参数
*/
//private String openid;
/**
* 4、进行人脸识别
* 必填:否
* 目标face_code类型可选值"1"刷卡付款码18位数字通过「付款码支付/被扫支付」接口完成支付,
* 如果不填写则默认为"0"人脸付款码数字字母混合通过「刷脸支付」接口完成支付。1.16版本以上支持该参数
*/
private String face_code_type;
/**
* 4、进行人脸识别
* 必填:否
* 订单金额(数字), 单位分该字段在在face_code_type为"1"时可不填,为"0"时必填
*/
private String total_fee;
/**
* 4、进行人脸识别
* 必填:否
* 商户订单号须与调用支付接口时字段一致该字段在在face_code_type为"1"时可不填,为"0"时必填
*/
private String out_trade_no;
/**
* 4、进行人脸识别
* 必填:是
* FACEPAY: 人脸凭证(face_code),用于刷脸支付 FACE_AUTH: 实名认证(需联系微信支付开通权限)
*/
private String face_authtype;
/**
* 4、进行人脸识别
* 必填:是
* 调用凭证
*/
private String authinfo;
/**
* 4、进行人脸识别
* 必填:否
* 指定刷脸界面显示的屏幕编号。
* 编号1为主屏幕其余屏幕按系统设置中的顺序从2开始编号常用场景举例双屏机器上传"2"即可显示在副屏上。如果不填写则默认显示在主屏幕。1.18版本以上支持该参数
*/
//private String screen_index;
/**
* 4、进行人脸识别
* 必填:否
* 设置不接受外接键盘输入,可选值:"1"禁用。1.23版本以上支持该参数
*/
//private String disable_keyboard;
/**
* 4、进行人脸识别
* 必填:否
* 设置刷脸窗口以无焦点方式启动,可选值:"1"无焦点启动窗口。1.26版本以上支持该参数
*/
//private String use_window_nofocus;
/**
* 8、更新支付结果
* 支付结果。可取值SUCCESS: 支付成功、ERROR: 支付失败
*/
private String payresult;
//构建
public WxFacePayReq(String cmd, String version, long now) {
this.cmd= cmd;
this.version =version;
this.now = now;
}
//构建
public WxFacePayReq(String cmd, String version, long now,int camera_rotation) {
this.cmd= cmd;
this.version =version;
this.now = now;
this.camera_rotation = camera_rotation;
}
}

View File

@@ -0,0 +1,40 @@
package com.dpkj.modules.scanface.wx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @description:WxFacePayResp 调用微信刷脸的返回值
* @author: Zhangxue
* @time: 2024/11/29 17:18
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class WxFacePayResp {
/**
* 错误码
*/
private String return_code;
/**
* 对错误码的描述
*/
private String return_msg;
/**
* 版本号
*/
private String version;
/**
* 2、获取数据
* 初始化数据。用于接口调用获取authinfo
*/
private String rawdata;
}

View File

@@ -1,4 +1,9 @@
server:
port: 5948
dpkj:
#后端项目访问地址 #https://yinyitong.yzqingyan.cn/ http://172.16.11.13:15946/ ttps://yinyitong.yzqingyan.cn
serverurl: http://localhost:5946/api/
# 医保配置
chs:
# 医保机构编码
@@ -10,8 +15,81 @@ dpkj:
printer:
# 打印机连接方式 USB: usb连接 | BTMS串口连接
connection-type: USB
# 终端号设置
terminal-Number: PORT-000000001
# 打印时间格式化
time-Type: yyyy-MM-dd HH:mm:ss
# 打印端口 串口连接下使用
port-name:
# 波特率 串口连接下使用
baud-rate:
# 指定打印机第一层是a4还是a5
level-one: A5
#支付宝刷脸
ali:
face:
#dll文件路径
dll-path: C:/opt/ant-abcp/bpaas_api.dll
#IOT 应用管理-appid
app-id: 2021005138656502
#IOT应用版本
app-version: 1.0.0.0
#签约商家的 PID以 2088 开头,企业主体
merchant-id: 2088641941653700
#商家机具终端编号,每台设备保持唯一
device-num: P060003750
#服务商的 PID
partner-id: 2088641941653700
# 核心入参 serviceId
service-id: pay
#微信模块
wx:
configs: #清研家
- app-id: wxe8334dd2140bb0e1
# 公众号的appsecret
secret: f83420d79cc6ecd1d7fe9684ac9cdfe4
# 接口配置里的Token值
token: dpkjylwjvote
# 接口配置里的EncodingAESKey值
aes-key: go2uM3ASe2rEyeoNsZHoPCiKGgpku0Bi49P5IypdQWT
mch-config: #商户信息 清研家关联的商户:驿路万家-扬州清研软件科技
app-id: wxe8334dd2140bb0e1
secret: f83420d79cc6ecd1d7fe9684ac9cdfe4
#调用接口所需service_id
service-id: service_id
#商户号
mch-id: 1557642321
#商户秘钥
mch-key: yndpkj15288216506YndpkjKsjytZx12
#报文解密 APIv3密钥
v3-key: yndpkj15288216506YndpkjKsjytZx12
#微信: 商户APIv2密钥
key-api: yndpkj15288216506YndpkjKsjytZx12
#p12证书的位置可以指定绝对路径也可以指定类路径以classpath;开头)
key-path: classpath:test\apiclient_cert.p12
private-key-path: classpath:test\apiclient_key.pem
private-cert-path: classpath:test\apiclient_cert.pem
#微信商家api序列号
mch-serial-no:
#JSAPI--公众号支付 NATIVE--原生扫码支付 APP--app支付
trade-type: JSAPI
trade-type-native: NATIVE
#支付回调方法地址
pay-notify-url: ${dpkj.url.server-url}/openapi/wxPayOrderApi/notify
#退款回调方法地址
refund-notify-url: ${dpkj.url.server-url}/openapi/wxPayOrderApi/refundNotify
# 路径
url:
# 后端接口地址
# server: https://102760424tfyw.vicp.fun/api
server: ${dpkj.url.server-url}
# h5地址
h5: ${dpkj.url.h5-url}
# 自定义app参数
app:
custom:
lexMarkServiceIp: http://127.0.0.1
lexMarkServicePort: 12346

View File

@@ -1,4 +1,10 @@
server:
port: 5946
dpkj:
#后端项目访问地址
#TODO 改为正式的地址 http://www.lczyyy.com/api http://127.0.0.1:15946/api
serverurl: http://www.lczyyy.com/api
# 医保配置
chs:
# 医保机构编码
@@ -10,8 +16,79 @@ dpkj:
printer:
# 打印机连接方式 USB: usb连接 | BTMS串口连接
connection-type: USB
# 终端号设置
terminal-Number: PORT-000000001
# 打印时间格式化
time-Type: yyyy-MM-dd HH:mm:ss
# 打印端口 串口连接下使用
port-name:
# 波特率 串口连接下使用
baud-rate:
# 指定打印机第一层是a4还是a5
level-one: A5
#支付宝刷脸 中医院配置
ali:
face:
#dll文件路径
dll-path: C:/opt/ant-abcp/bpaas_api.dll
#IOT 应用管理-appid
app-id: 2021005151604729
#IOT应用版本
app-version: 1.0.0.0
#签约商家的 PID以 2088 开头,企业主体
merchant-id: 2088170977486823
#商家机具终端编号,每台设备保持唯一
device-num: P060003750
#服务商的 PID
partner-id: 2088170977486823
# 核心入参 serviceId
service-id: pay
#微信模块
wx:
configs: # 清研家
- app-id: wxc12fa4977f66974d #wxc12fa4977f66974d #wx024903d1d2e1a55a
# 公众号的appsecret
secret: a51dbcac25a73d7e4812a43cf550c5fc #f83420d79cc6ecd1d7fe9684ac9cdfe4
# 接口配置里的Token值 DPKJ-YINYITONG #DPKJ-YINYITONG#dpkjylwjvote
token: DPKJ-YINYITONG
# 接口配置里的EncodingAESKey值
aes-key: yIBgBrHwRGjO2L3CLIE9hmnlf1FrXQQ7qJZVIg4r6Dx
mch-config: #商户信息 驿路万家-扬州清研软件科技
app-id: wxc12fa4977f66974d
secret: f83420d79cc6ecd1d7fe9684ac9cdfe4
#调用接口所需service_id
service-id: service_id
#商户号
mch-id: 1557642321
#商户秘钥
mch-key: yndpkj15288216506YndpkjKsjytZx12
#报文解密 APIv3密钥
v3-key: yndpkj15288216506YndpkjKsjytZx12
#微信: 商户APIv2密钥
key-api: yndpkj15288216506YndpkjKsjytZx12
#p12证书的位置可以指定绝对路径也可以指定类路径以classpath;开头)
key-path: classpath:test\apiclient_cert.p12
private-key-path: classpath:test\apiclient_key.pem
private-cert-path: classpath:test\apiclient_cert.pem
#微信商家api序列号
mch-serial-no:
#JSAPI--公众号支付 NATIVE--原生扫码支付 APP--app支付
trade-type: JSAPI
trade-type-native: NATIVE
#支付回调方法地址
pay-notify-url: ${dpkj.url.server-url}/openapi/wxPayOrderApi/notify
#退款回调方法地址
refund-notify-url: ${dpkj.url.server-url}/openapi/wxPayOrderApi/refundNotify
# 路径
url:
# 后端接口地址
# server: https://102760424tfyw.vicp.fun/api
server: ${dpkj.url.server-url}
# h5地址
h5: ${dpkj.url.h5-url}
# 自定义app参数
app:
custom:
lexMarkServiceIp: http://127.0.0.1
lexMarkServicePort: 12346

View File

@@ -1,18 +1,4 @@
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
resource:
static-locations: classpath:/static/,classpath:/public/
application:
name: ems-express-bridge
profiles:
#active: '@profile.name@'
active: pro
server:
port: 5946
servlet:
context-path: /api
tomcat:
@@ -26,11 +12,18 @@ server:
min-response-size: 1024
mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*
logging:
level:
com.dpkj: debug
# 自定义app参数
app:
custom:
lexMarkServiceIp: http://127.0.0.1
lexMarkServicePort: 12346
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
resource:
static-locations: classpath:/static/,classpath:/public/
application:
name: ems-express-bridge
profiles:
active: dev

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 -->
<property name="LOG_HOME" value="./logs/java"/>
<property name="LOG_HOME" value="${log.path:-.}/logs"/>
<!--<property name="COLOR_PATTERN" value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta( %replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''})- %gray(%msg%xEx%n)" />-->
<!-- 控制台输出 -->
@@ -70,8 +70,8 @@
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
<appender-ref ref="HTML"/>
<appender-ref ref="FILE_HTML"/>
<!-- <appender-ref ref="HTML"/>-->
<!-- <appender-ref ref="FILE_HTML"/>-->
</root>
</configuration>

View File

@@ -2,202 +2,71 @@
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>门诊缴费凭证</title>
<style>
.bold-text {
font-weight: 700;
}
.text-35 {
font-size: 35px;
}
.text-30 {
font-size: 30px;
}
.center-text {
text-align: center;
}
.margin-top-10 {
margin-top: 10px;
}
.margin-top-40 {
margin-top: 40px;
}
.header-title {
font-size: 28px;
}
.header-terminal {
font-size: 38px;
margin-top: 20px;
}
.header-receipt {
font-size: 38px;
margin-top: 10px;
}
.divider {
text-align: center;
font-weight: 700;
font-size: 32px;
}
.patient-info {
font-size: 35px;
margin: 10px 20px;
}
.patient-info .info-item {
flex: 1;
}
.patient-info .info-row {
display: flex;
}
.payment-info {
font-size: 35px;
margin: 10px 20px;
}
.payment-info .info-item {
flex: 1;
}
.payment-info .info-row {
display: flex;
}
.table-container {
margin: 10px 20px;
}
.table-container table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.table-container .first-th {
width: 300px;
text-align: left;
}
.table-container .df-th {
width: calc(25% - 5px);
text-align: center;
}
.table-container .first-td {
text-align: left;
width: 300px;
word-break: break-all;
}
.table-container .df-td {
text-align: center;
}
.terminal-info {
margin-left: 20px;
font-weight: 700;
font-size: 35px;
}
.tips {
margin-left: 20px;
font-size: 35px;
font-weight: 700;
margin-top: 20px;
}
</style>
<title>门诊缴费凭证T2</title>
</head>
<body>
<div class="bold-text">
<div class="center-text margin-top-40">
<div><span class="header-title">******<span th:text="${hospitalName}"></span>******</span></div>
<div class="header-terminal"><span th:text="${registeTerminalName}"></span></div>
<div class="header-receipt"><span th:text="${registeType}"></span></div>
<div style="font-size: 32px;font-weight: bold;">
<div>
<div style="text-align: center; font-size: 40px; "><span th:text="${hospitalName}"></span>
</div>
<div style="text-align: center; font-size: 28px;"><span>****<span
th:text="${registeTerminalName}"></span>****</span>
</div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
<div style="text-align: center; font-size: 40px; "><span th:text="${registeType}"></span></div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
</div>
<div class="divider">
<div style="font-size: 35px;word-break: break-all;margin: 10px 20px;">
<div style="display: flex;">
<span style="margin-right: 25px;">姓名:<span th:text="${name}"></span></span>
<span style="margin-right: 25px;">性别:<span th:text="${gender}"></span></span>
<span>年龄:<span th:text="${age}"></span></span>
</div>
<div style="word-break: break-all;margin-top: 10px;">
<span style="margin-right: 25px;" th:if="${outpatientNumber}">
门诊号:<span th:text="${outpatientNumber}"></span></span>
<span th:if="${doctor}">
就诊医生:<span th:text="${doctor}"></span>
</span>
</div>
<div style="margin-top: 10px;" th:if="${department}">就诊科室:<span th:text="${department}"></span>
</div>
</div>
<div style="text-align: center; font-size: 32px;">
-----------------------------------------------------------------------------
</div>
<div class="patient-info">
<div class="info-row">
<div class="info-item">姓名<span th:text="${name}"></span></div>
<div class="info-item">性别<span th:text="${gender}"></span></div>
<div class="info-item">年龄:<span th:text="${age}"></span></div>
<div style="font-size: 35px; margin: 10px 20px">
<div style="word-break: break-all;">
<span style="margin-right: 25px;">费用总额<span th:text="${totalFee}"></span></span>
<span>个人支付<span th:text="${personalPayment}"></span></span>
</div>
<div class="info-row margin-top-10">
<div class="info-item" th:if="${outpatientNumber}">门诊号:<span th:text="${outpatientNumber}"></span></div>
<div class="info-item" th:if="${doctor}">就诊医生:<span th:text="${doctor}"></span></div>
</div>
<div class="margin-top-10" th:if="${department}">就诊科室:<span th:text="${department}"></span></div>
<div style="margin-top: 10px;word-break: break-all;">
<span>实收金额:</span><span
th:text="${actualReceiptAmount}"></span></div>
<div style="margin-top: 10px;"><span>实收金额:</span><span th:text="${actualReceiptAmountChinese}"></span></div>
</div>
<div class="divider">
-----------------------------------------------------------------------------
</div>
<div class="payment-info">
<div class="info-row">
<div class="info-item">费用总额:<span th:text="${totalFee}"></span></div>
<div class="info-item">个人支付:<span th:text="${personalPayment}"></span></div>
</div>
<div class="margin-top-10">实收金额:<span th:text="${actualReceiptAmount}"></span></div>
<div class="margin-top-10">实收金额:<span th:text="${actualReceiptAmountChinese}"></span></div>
</div>
<div class="divider">
-----------------------------------------------------------------------------
</div>
<div style="margin-left: 20px; margin-top: 10px; ">
<table border="0" style="font-size: 30px;">
<tr style="font-size: 34px; ">
<th>项目名称</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
<div style="margin: 10px 20px">
<table style="width: 100%; table-layout: fixed; border-collapse: collapse;">
<tr style="font-size: 35px;">
<th style="width: 300px;text-align: left;">项目名称</th>
<th style="width: calc(25% - 5px);text-align: center;">数量</th>
<th style="width: calc(25% - 5px);text-align: center;">单价</th>
<th style="width: calc(25% - 5px);text-align: center;">小计</th>
</tr>
<tr th:each="item : ${items}" style="height: 50px;">
<td style=" width: 310px; word-break: break-all;" th:text="${item.name}"></td>
<td th:text="${item.quantity}"></td>
<td th:text="${item.unitPrice}"></td>
<td th:text="${item.subtotal}"></td>
<tr style="font-size: 30px;word-break: break-all;" th:each="item : ${items}">
<td style="text-align: left;width: 300px;" th:text="${item.name}"></td>
<td style="text-align: center;" th:text="${item.quantity}"></td>
<td style="text-align: center;" th:text="${item.unitPrice}"></td>
<td style="text-align: center;" th:text="${item.subtotal}"></td>
</tr>
</table>
</div>
<div class="table-container">
<table>
<tr class="text-35">
<th class="first-th">项目名称</th>
<th class="df-th">数量</th>
<th class="df-th">单价</th>
<th class="df-th">小计</th>
</tr>
<tr class="text-30" th:each="item : ${items}">
<td class="first-td" th:text="${item.name}"></td>
<td class="df-td" th:text="${item.quantity}"></td>
<td class="df-td" th:text="${item.unitPrice}"></td>
<td class="df-td" th:text="${item.subtotal}"></td>
</tr>
</table>
</div>
<div class="divider">
-----------------------------------------------------------------------------
</div>
<div class="terminal-info">
<div>终端编号:<span th:text="${terminalNumber}"></span></div>
<div>打印时间:<span th:text="${printTime}"></span></div>
</div>
<div class="tips">
<span>温馨提示</span><br>
<span>1.请取走全部凭条、并妥善保管</span><br>
<span>2.如果对缴费结算存在疑问,请到人工窗口咨询</span>
<div style="border-bottom: 1px dashed #000;margin-bottom: 3px;"></div>
<div style="margin-left: 35px;font-size: 36px;">终端编号:<span th:text="${terminalNumber}"></span></div>
<div style="margin-left: 35px;font-size: 36px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="margin-left: 35px;font-size: 36px;margin-top: 10px;">
<span>温馨提示:请取走缴费凭证,并妥善保管。如果对缴费存在疑问,请到人工窗口咨询!</span>
</div>
</div>
</body>

View File

@@ -4,15 +4,18 @@
<title>挂号单</title>
</head>
<body>
<div style="font-size: 32px; margin-top: 20px;">
<div style="">
<div style="text-align: center; font-size: 40px; font-weight: 700;"><span th:text="${hospitalName}"></span></div>
<div style="text-align: center; font-weight: 700; font-size: 28px;">****<span th:text="${registeTerminalName}"></span></span>****</div>
<div style="text-align: center; font-weight: 700; margin-bottom: -10px; margin-top: -10px;">-----------------------------------------------------------------------------</div>
<div style="text-align: center; font-size: 40px; font-weight: 700;"><span th:text="${registeType}"></span></div>
<div style="text-align: center; font-weight: 700; margin-top: -15px;">-----------------------------------------------------------------------------</div>
<div style="font-size: 32px;font-weight: bold;">
<div>
<div style="text-align: center; font-size: 40px; "><span th:text="${hospitalName}"></span>
</div>
<div style="text-align: center; font-size: 28px;"><span>****<span
th:text="${registeTerminalName}"></span>****</span>
</div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
<div style="text-align: center; font-size: 40px; "><span th:text="${registeType}"></span></div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
</div>
<div style="font-weight: 700; font-size: 37px;">
<div style=" font-size: 37px;">
<div style="margin-left: 35px;">&emsp;&emsp;名:<span th:text="${name}"></span></div>
<div style="margin-left: 35px;">&emsp;&emsp;别:<span th:text="${gender}"></span></div>
<div style="margin-left: 35px;">&emsp;&emsp;龄:<span th:text="${age}"></span></div>
@@ -21,13 +24,14 @@
<div style="margin-left: 35px;">入院科室:<span th:text="${department}"></span></div>
<div style="margin-left: 35px;">&ensp;&ensp;用:<span th:text="${totalFee}"></span>&thinsp;</div>
<div style="margin-left: 35px;">支付方式:<span th:text="${paymentMethod}"></span></div>
<div style="margin-left: 35px;">交易流水:<span style="font-size: 35px;" th:text="${transactionNumber}"></span></div>
<div style="margin-left: 35px;">交易流水:<span style="font-size: 35px;" th:text="${transactionNumber}"></span>
</div>
</div>
<div style="font-weight: 700; margin-top: 8px;">
<!-- <div style="margin-top: -40px; text-align: center; ">-&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;</div>-->
<div style="font-size: 36px;margin-left: 35px; ">备注:缴费凭证,请妥善保管!</div>
<div style="font-size: 36px;margin-left: 35px;">&ensp;&ensp;号:<span th:text="${terminalNumber}"></span></div>
<div style="font-size: 36px;margin-left: 35px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="border-bottom: 1px dashed #000;margin-bottom: 3px;"></div>
<div style="margin-left: 35px;font-size: 36px;">终端编号:<span th:text="${terminalNumber}"></span></div>
<div style="margin-left: 35px;font-size: 36px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="margin-left: 35px;font-size: 36px;margin-top: 10px;">
<span>温馨提示:请取走缴费凭证,并妥善保管。如果对缴费存在疑问,请到人工窗口咨询!</span>
</div>
</div>
</body>

View File

@@ -1,16 +1,19 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>门诊缴费</title>
<title>门诊缴费凭证T1</title>
</head>
<body>
<div style="font-size: 32px; margin-top: 20px;">
<div style="">
<div style="text-align: center; font-size: 40px; font-weight: 700;"><span th:text="${hospitalName}"></span></div>
<div style="text-align: center; font-weight: 700; font-size: 28px;">****<span th:text="${registeTerminalName}"></span></span>****</div>
<div style="text-align: center; font-weight: 700; margin-bottom: -10px; margin-top: -10px;">-----------------------------------------------------------------------------</div>
<div style="text-align: center; font-size: 40px; font-weight: 700;"><span th:text="${registeType}"></span></div>
<div style="text-align: center; font-weight: 700; margin-top: -15px;">-----------------------------------------------------------------------------</div>
<div style="font-size: 32px;font-weight: bold;">
<div>
<div style="text-align: center; font-size: 40px; "><span th:text="${hospitalName}"></span>
</div>
<div style="text-align: center; font-size: 28px;"><span>****<span
th:text="${registeTerminalName}"></span>****</span>
</div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
<div style="text-align: center; font-size: 40px; "><span th:text="${registeType}"></span></div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
</div>
<div style="font-weight: 700; font-size: 37px;">
<div style="margin-left: 35px;">&emsp;&emsp;名:<span th:text="${name}"></span></div>
@@ -19,13 +22,14 @@
<div style="margin-left: 35px;">出生日期:<span th:text="${birthDate}"></span></div>
<div style="margin-left: 35px;">&ensp;&ensp;用:<span th:text="${totalFee}"></span>&thinsp;</div>
<div style="margin-left: 35px;">支付方式:<span th:text="${paymentMethod}"></span></div>
<div style="margin-left: 35px;">交易流水:<span style="font-size: 35px;" th:text="${transactionNumber}"></span></div>
<div style="margin-left: 35px;">交易流水:<span style="font-size: 35px;" th:text="${transactionNumber}"></span>
</div>
</div>
<div style="font-weight: 700; margin-top: 8px;">
<!-- <div style="margin-top: -40px; text-align: center; ">-&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;</div>-->
<div style="font-size: 36px;margin-left: 35px; ">备注:缴费凭证,请妥善保管!</div>
<div style="font-size: 36px;margin-left: 35px;">&ensp;&ensp;号:<span th:text="${terminalNumber}"></span></div>
<div style="font-size: 36px;margin-left: 35px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="border-bottom: 1px dashed #000;margin-bottom: 3px;"></div>
<div style="margin-left: 35px;font-size: 36px;">终端编号:<span th:text="${terminalNumber}"></span></div>
<div style="margin-left: 35px;font-size: 36px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="margin-left: 35px;font-size: 36px;margin-top: 10px;">
<span>温馨提示:请取走缴费凭证,并妥善保管。如果对缴费存在疑问,请到人工窗口咨询!</span>
</div>
</div>
</body>

View File

@@ -3,38 +3,23 @@
<head>
<title>挂号单</title>
</head>
<style>
.x-center {
text-align: center;
}
.x-start {
text-align: left;
}
.x-end {
text-align: right;
}
.x-bold {
font-weight: 700;
}
</style>
<body>
<div style="font-size: 32px; margin-top: 20px;">
<div style="">
<div style="text-align: center; font-size: 40px; font-weight: 700;"><span th:text="${hospitalName}"></span></div>
<div style="text-align: center; font-weight: 700; font-size: 28px;">****<span th:text="${registeTerminalName}"></span></span>****</div>
<div style="text-align: center; font-weight: 700; margin-bottom: -10px; margin-top: -10px;">-----------------------------------------------------------------------------</div>
<div style="text-align: center; font-size: 40px; font-weight: 700;"><span th:text="${registeType}"></span></div>
<div style="text-align: center; font-weight: 700; margin-top: -15px;">-----------------------------------------------------------------------------</div>
<div style="font-size: 32px;font-weight: bold;">
<div>
<div style="text-align: center; font-size: 40px; "><span th:text="${hospitalName}"></span>
</div>
<div style="text-align: center; font-size: 28px;"><span>****<span
th:text="${registeTerminalName}"></span>****</span>
</div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
<div style="text-align: center; font-size: 40px; "><span th:text="${registeType}"></span></div>
<div style="border-bottom: 1px dashed #000;margin: 3px 0;"></div>
</div>
<div style="font-weight: 700; font-size: 37px;">
<div style=" font-size: 37px;">
<div style="margin-left: 35px;">&emsp;&emsp;名:<span th:text="${name}"></span></div>
<div style="margin-left: 35px;">&emsp;&emsp;别:<span th:text="${gender}"></span></div>
<div style="margin-left: 35px;">&emsp;&emsp;龄:<span th:text="${age}"></span></div>
<div style="margin-left: 35px;">出生日期:<span th:text="${birthDate}"></span></div>
<!-- <div style="margin-left: 35px;">卡&emsp;&emsp;号:<span th:text="${cardNumber}"></span></div>-->
<div style="margin-left: 35px;">&ensp;&ensp;号:<span th:text="${outpatientNumber}"></span></div>
<div style="margin-left: 35px;">就诊科室:<span th:text="${department}"></span></div>
<div style="margin-left: 35px;">出诊级别:<span th:text="${visitLevel}"></span></div>
@@ -43,20 +28,18 @@
<div style="margin-left: 35px;">挂号日期:<span th:text="${registerDate}"></span></div>
<div style="margin-left: 35px;">&ensp;&ensp;用:<span th:text="${totalFee}"></span>&thinsp;</div>
<div style="margin-left: 35px;">支付方式:<span th:text="${paymentMethod}"></span></div>
<!-- <div style="margin-left: 35px;">订&ensp;单&ensp;号:<span style="font-size: 36px;" th:text="${orderNumber}"></span></div>-->
<div style="margin-left: 35px;">交易流水:<span style="font-size: 35px;"
th:text="${transactionNumber}"></span></div>
<div style="width: 100%; text-align: center; margin-bottom: -20px;">
<img style="display: inline-block; "
th:src="${qrCodeBase64}"
alt="QR Code" />
<div style="width:100%;text-align: center;">
<img th:src="${qrCodeBase64}"
alt="#"/>
</div>
</div>
<div style="font-weight: 700; margin-top: 8px;">
<div style="margin-top: -40px; text-align: center; ">-----------------------------------------------------------------------------</div>
<div style="font-size: 36px;margin-left: 35px; ">备注:凭此条退费,请妥善保管!</div>
<div style="font-size: 36px;margin-left: 35px;">&ensp;&ensp;号:<span th:text="${terminalNumber}"></span></div>
<div style="font-size: 36px;margin-left: 35px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="border-bottom: 1px dashed #000;margin-bottom: 3px;"></div>
<div style="margin-left: 35px;font-size: 36px;">终端编号:<span th:text="${terminalNumber}"></span></div>
<div style="margin-left: 35px;font-size: 36px;">打印时间:<span th:text="${printTime}"></span></div>
<div style="margin-left: 35px;font-size: 36px;margin-top: 10px;">
<span>温馨提示:请取走挂号凭证,并妥善保管。如果对缴费存在疑问,请到人工窗口咨询!</span>
</div>
</div>
</body>

Binary file not shown.

Binary file not shown.