From 6affaba5c3a1b88ec8935cb10486b6ddf5137da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 17 Dec 2025 14:48:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(credit):=20=E6=96=B0=E5=A2=9E=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增企业实体类CreditCompany,包含企业基本信息字段 - 新增企业控制器CreditCompanyController,提供CRUD接口 - 新增企业导入参数类CreditCompanyImportParam,支持Excel导入 - 新增企业查询参数类CreditCompanyParam,支持条件查询 - 新增企业Mapper接口及XML映射文件,实现关联查询 - 实现企业分页查询、列表查询、详情查询接口 - 实现企业新增、修改、删除接口 - 实现企业批量导入功能,支持Excel模板下载 - 实现企业数据校验和重复数据处理逻辑 - 添加企业导入模板下载接口 - 支持企业信息的完整字段映射和转换逻辑 - 添加企业查询条件注解,支持动态SQL查询 - 实现企业关联查询SQL,支持多字段模糊匹配 - 添加企业操作日志记录和权限控制注解 - 完善企业导入异常处理和错误信息收集机制 --- .../credit/mapper/xml/CreditUserMapper.xml | 2 + .../led/config/BmeApiProperties.java | 49 +++ .../led/controller/LedApiController.java | 37 +++ .../com/gxwebsoft/led/model/BmeToken.java | 28 ++ .../led/param/BmeNumberSourcesParam.java | 48 +++ .../led/param/BmeStopReplaceParam.java | 35 +++ .../led/service/LedScheduleService.java | 288 ++++++++++++++++++ src/main/resources/application.yml | 11 + 8 files changed, 498 insertions(+) create mode 100644 src/main/java/com/gxwebsoft/led/config/BmeApiProperties.java create mode 100644 src/main/java/com/gxwebsoft/led/controller/LedApiController.java create mode 100644 src/main/java/com/gxwebsoft/led/model/BmeToken.java create mode 100644 src/main/java/com/gxwebsoft/led/param/BmeNumberSourcesParam.java create mode 100644 src/main/java/com/gxwebsoft/led/param/BmeStopReplaceParam.java create mode 100644 src/main/java/com/gxwebsoft/led/service/LedScheduleService.java diff --git a/src/main/java/com/gxwebsoft/credit/mapper/xml/CreditUserMapper.xml b/src/main/java/com/gxwebsoft/credit/mapper/xml/CreditUserMapper.xml index 7e00d8a..0e522b2 100644 --- a/src/main/java/com/gxwebsoft/credit/mapper/xml/CreditUserMapper.xml +++ b/src/main/java/com/gxwebsoft/credit/mapper/xml/CreditUserMapper.xml @@ -84,6 +84,8 @@ AND (a.name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.procurement_name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.winning_name LIKE CONCAT('%', #{param.keywords}, '%') ) diff --git a/src/main/java/com/gxwebsoft/led/config/BmeApiProperties.java b/src/main/java/com/gxwebsoft/led/config/BmeApiProperties.java new file mode 100644 index 0000000..f0221d6 --- /dev/null +++ b/src/main/java/com/gxwebsoft/led/config/BmeApiProperties.java @@ -0,0 +1,49 @@ +package com.gxwebsoft.led.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 业务中台(排班接口)调用配置。 + */ +@Data +@Component +@ConfigurationProperties(prefix = "led.bme") +public class BmeApiProperties { + + /** + * 中台基础地址,例如:http://16.1.4.201:7979 + */ + private String baseUrl; + + /** + * 授权应用ID(文档中的 APPID)。 + */ + private String appid; + + /** + * 应用密钥(文档中的 secret_key)。 + */ + private String secretKey; + + /** + * 机构ID,默认按文档要求传 10001。 + */ + private String mechanismId = "10001"; + + /** + * 默认操作员代码。 + */ + private String defaultExtUserId; + + /** + * 默认医院代码,可留空。 + */ + private String defaultHospitalId; + + /** + * 连接/读取超时时间(毫秒)。 + */ + private int timeoutMs = 10000; +} diff --git a/src/main/java/com/gxwebsoft/led/controller/LedApiController.java b/src/main/java/com/gxwebsoft/led/controller/LedApiController.java new file mode 100644 index 0000000..0335e5e --- /dev/null +++ b/src/main/java/com/gxwebsoft/led/controller/LedApiController.java @@ -0,0 +1,37 @@ +package com.gxwebsoft.led.controller; + +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.led.param.BmeNumberSourcesParam; +import com.gxwebsoft.led.param.BmeStopReplaceParam; +import com.gxwebsoft.led.service.LedScheduleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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.RestController; + +import javax.annotation.Resource; + +@Tag(name = "LED-排班接口") +@RestController +@RequestMapping("/api/led/bme") +public class LedApiController extends BaseController { + + @Resource + private LedScheduleService ledScheduleService; + + @Operation(summary = "查询一周内停替诊医生排班信息(10017)") + @PostMapping("/stop-replace") + public ApiResult searchStopAndReplace(@RequestBody BmeStopReplaceParam param) { + return success(ledScheduleService.searchStopAndReplace(param)); + } + + @Operation(summary = "查询医生排班信息当日剩余号源(10018)") + @PostMapping("/number-sources") + public ApiResult searchNumberSources(@RequestBody BmeNumberSourcesParam param) { + return success(ledScheduleService.searchNumberSources(param)); + } +} diff --git a/src/main/java/com/gxwebsoft/led/model/BmeToken.java b/src/main/java/com/gxwebsoft/led/model/BmeToken.java new file mode 100644 index 0000000..9ac429d --- /dev/null +++ b/src/main/java/com/gxwebsoft/led/model/BmeToken.java @@ -0,0 +1,28 @@ +package com.gxwebsoft.led.model; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 业务中台授权令牌缓存实体。 + */ +@Data +public class BmeToken implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 接口调用凭证 access_token。 + */ + private String accessToken; + + /** + * 刷新 access_token 的 refresh_token。 + */ + private String refreshToken; + + /** + * access_token 过期时间戳(秒级)。 + */ + private long expireAt; +} diff --git a/src/main/java/com/gxwebsoft/led/param/BmeNumberSourcesParam.java b/src/main/java/com/gxwebsoft/led/param/BmeNumberSourcesParam.java new file mode 100644 index 0000000..9391be3 --- /dev/null +++ b/src/main/java/com/gxwebsoft/led/param/BmeNumberSourcesParam.java @@ -0,0 +1,48 @@ +package com.gxwebsoft.led.param; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 查询医生排班信息当日剩余号源(10018)入参。 + */ +@Data +public class BmeNumberSourcesParam { + + @Schema(description = "开始日期,yyyy-MM-dd") + private String startDate; + + @Schema(description = "结束日期,yyyy-MM-dd") + private String endDate; + + @Schema(description = "科室代码,可选") + private String deptCode; + + @Schema(description = "医生工号,可选") + private String userCode; + + @Schema(description = "时段,可选:01上午 02下午 03全天 04中午") + @JsonProperty("aSTimeRange") + private String aSTimeRange; + + @Schema(description = "医院代码,可选") + private String hospitalId; + + @Schema(description = "机构ID,默认10001") + private String mechanismId; + + @Schema(description = "操作员代码") + private String extUserID; + + @Schema(description = "交易代码,默认10018") + private String tradeCode; + + public String getASTimeRange() { + return aSTimeRange; + } + + public void setASTimeRange(String aSTimeRange) { + this.aSTimeRange = aSTimeRange; + } +} diff --git a/src/main/java/com/gxwebsoft/led/param/BmeStopReplaceParam.java b/src/main/java/com/gxwebsoft/led/param/BmeStopReplaceParam.java new file mode 100644 index 0000000..27a53c1 --- /dev/null +++ b/src/main/java/com/gxwebsoft/led/param/BmeStopReplaceParam.java @@ -0,0 +1,35 @@ +package com.gxwebsoft.led.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 查询一周内停替诊排班(10017)入参。 + */ +@Data +public class BmeStopReplaceParam { + + @Schema(description = "开始日期,yyyy-MM-dd") + private String startDate; + + @Schema(description = "结束日期,yyyy-MM-dd") + private String endDate; + + @Schema(description = "科室代码,可选") + private String deptCode; + + @Schema(description = "医生工号,可选") + private String userCode; + + @Schema(description = "医院代码,可选") + private String hospitalId; + + @Schema(description = "机构ID,默认10001") + private String mechanismId; + + @Schema(description = "操作员代码") + private String extUserID; + + @Schema(description = "交易代码,默认10017") + private String tradeCode; +} diff --git a/src/main/java/com/gxwebsoft/led/service/LedScheduleService.java b/src/main/java/com/gxwebsoft/led/service/LedScheduleService.java new file mode 100644 index 0000000..c00bc48 --- /dev/null +++ b/src/main/java/com/gxwebsoft/led/service/LedScheduleService.java @@ -0,0 +1,288 @@ +package com.gxwebsoft.led.service; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.common.core.exception.BusinessException; +import com.gxwebsoft.common.core.utils.JSONUtil; +import com.gxwebsoft.common.core.utils.RedisUtil; +import com.gxwebsoft.led.config.BmeApiProperties; +import com.gxwebsoft.led.model.BmeToken; +import com.gxwebsoft.led.param.BmeNumberSourcesParam; +import com.gxwebsoft.led.param.BmeStopReplaceParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 排班接口对接服务。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LedScheduleService { + + private static final String TOKEN_CACHE_KEY = "led:bme:auth"; + private static final long TOKEN_EXPIRE_MARGIN_SECONDS = 60L; + + private final BmeApiProperties properties; + private final RedisUtil redisUtil; + + public JSONObject searchStopAndReplace(BmeStopReplaceParam param) { + BmeStopReplaceParam requestParam = fillStopReplaceDefaults(param); + BmeToken token = ensureToken(); + System.out.println("token = " + token); + String body = buildStopReplaceBody(requestParam); + return doSignedPost("/cgi-bin/bme-app-register/outpatient/registered/searchDoctorsStopAndReplaceInfo", body, token); + } + + public JSONObject searchNumberSources(BmeNumberSourcesParam param) { + BmeNumberSourcesParam requestParam = fillNumberSourcesDefaults(param); + BmeToken token = ensureToken(); + String body = buildNumberSourcesBody(requestParam); + return doSignedPost("/cgi-bin/bme-app-register/outpatient/registered/searchDoctorsNumberSources", body, token); + } + + private JSONObject doSignedPost(String path, String body, BmeToken token) { + if (StrUtil.isBlank(body)) { + throw new BusinessException("请求体为空"); + } + ensureConfig(); + long timestamp = System.currentTimeMillis() / 1000; + String nonce = RandomUtil.randomString(16); + String signStr = body + "|" + token.getAccessToken() + "|" + token.getRefreshToken() + "|" + timestamp + "|" + nonce; + String sig = DigestUtil.md5Hex(signStr); + HttpResponse response = HttpRequest.post(buildUrl(path)) + .header("sig", sig) + .header("timestamp", String.valueOf(timestamp)) + .header("nonce", nonce) + .header("access-token", token.getAccessToken()) + .contentType("application/json") + .body(body) + .timeout(properties.getTimeoutMs()) + .execute(); + if (response.getStatus() != 200) { + throw new BusinessException("调用中台接口失败,HTTP状态码:" + response.getStatus()); + } + String respBody = response.body(); + JSONObject respJson; + try { + respJson = JSON.parseObject(respBody); + } catch (Exception e) { + throw new BusinessException("解析中台响应失败"); + } + if (respJson == null || !"0".equals(respJson.getString("code"))) { + String message = respJson != null ? respJson.getString("msg") : "未知错误"; + throw new BusinessException("中台接口调用失败:" + message); + } + return respJson; + } + + private BmeStopReplaceParam fillStopReplaceDefaults(BmeStopReplaceParam param) { + if (param == null) { + throw new BusinessException("请求参数不能为空"); + } + if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) { + throw new BusinessException("开始日期和结束日期必填"); + } + if (StrUtil.isBlank(param.getMechanismId())) { + param.setMechanismId(properties.getMechanismId()); + } + if (StrUtil.isBlank(param.getExtUserID())) { + param.setExtUserID(properties.getDefaultExtUserId()); + } + if (StrUtil.isBlank(param.getHospitalId())) { + param.setHospitalId(properties.getDefaultHospitalId()); + } + if (StrUtil.isBlank(param.getTradeCode())) { + param.setTradeCode("10017"); + } + if (StrUtil.hasBlank(param.getMechanismId(), param.getExtUserID(), param.getTradeCode())) { + throw new BusinessException("机构ID、操作员代码、交易代码必填"); + } + return param; + } + + private BmeNumberSourcesParam fillNumberSourcesDefaults(BmeNumberSourcesParam param) { + if (param == null) { + throw new BusinessException("请求参数不能为空"); + } + if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) { + throw new BusinessException("开始日期和结束日期必填"); + } + if (StrUtil.isBlank(param.getMechanismId())) { + param.setMechanismId(properties.getMechanismId()); + } + if (StrUtil.isBlank(param.getExtUserID())) { + param.setExtUserID(properties.getDefaultExtUserId()); + } + if (StrUtil.isBlank(param.getHospitalId())) { + param.setHospitalId(properties.getDefaultHospitalId()); + } + if (StrUtil.isBlank(param.getTradeCode())) { + param.setTradeCode("10018"); + } + if (StrUtil.hasBlank(param.getMechanismId(), param.getExtUserID(), param.getTradeCode())) { + throw new BusinessException("机构ID、操作员代码、交易代码必填"); + } + return param; + } + + private String buildStopReplaceBody(BmeStopReplaceParam param) { + Map body = new LinkedHashMap<>(); + body.put("mechanismId", param.getMechanismId()); + body.put("tradeCode", param.getTradeCode()); + body.put("startDate", param.getStartDate()); + body.put("endDate", param.getEndDate()); + putIfNotBlank(body, "deptCode", param.getDeptCode()); + putIfNotBlank(body, "userCode", param.getUserCode()); + putIfNotBlank(body, "hospitalId", param.getHospitalId()); + body.put("extUserID", param.getExtUserID()); + return JSONUtil.toJSONString(body); + } + + private String buildNumberSourcesBody(BmeNumberSourcesParam param) { + Map body = new LinkedHashMap<>(); + body.put("mechanismId", param.getMechanismId()); + body.put("tradeCode", param.getTradeCode()); + body.put("startDate", param.getStartDate()); + body.put("endDate", param.getEndDate()); + putIfNotBlank(body, "deptCode", param.getDeptCode()); + putIfNotBlank(body, "userCode", param.getUserCode()); + putIfNotBlank(body, "aSTimeRange", param.getASTimeRange()); + putIfNotBlank(body, "hospitalId", param.getHospitalId()); + body.put("extUserID", param.getExtUserID()); + return JSONUtil.toJSONString(body); + } + + private void putIfNotBlank(Map body, String key, String value) { + if (StrUtil.isNotBlank(value)) { + body.put(key, value); + } + } + + private BmeToken ensureToken() { + ensureConfig(); + long now = System.currentTimeMillis() / 1000; + BmeToken cached = redisUtil.get(TOKEN_CACHE_KEY, BmeToken.class); + if (cached != null && cached.getExpireAt() > now + TOKEN_EXPIRE_MARGIN_SECONDS) { + return cached; + } + if (cached != null && StrUtil.isNotBlank(cached.getRefreshToken())) { + try { + return refreshToken(cached.getRefreshToken()); + } catch (Exception e) { + log.warn("刷新 access_token 失败,尝试重新申请: {}", e.getMessage()); + } + } + return fetchToken(); + } + + private BmeToken fetchToken() { + long timestamp = System.currentTimeMillis() / 1000; + String nonce = IdUtil.fastSimpleUUID(); + String grantType = "secret"; + Map form = new HashMap<>(); + form.put("appid", properties.getAppid()); + form.put("grant_type", grantType); + form.put("secret_key", properties.getSecretKey()); + form.put("timestamp", timestamp); + form.put("nonce", nonce); + String signStr = properties.getAppid() + "|" + properties.getSecretKey() + "|" + grantType + "|" + timestamp + "|" + nonce; + form.put("sign", DigestUtil.md5Hex(signStr)); + + HttpResponse response = HttpRequest.get(buildUrl("/cgi-bin/bme-auth/auth/access_token")) + .form(form) + .timeout(properties.getTimeoutMs()) + .execute(); + return handleTokenResponse(response); + } + + private BmeToken refreshToken(String refreshToken) { + long timestamp = System.currentTimeMillis() / 1000; + String nonce = IdUtil.fastSimpleUUID(); + String grantType = "refresh_token"; + Map form = new HashMap<>(); + form.put("appid", properties.getAppid()); + form.put("grant_type", grantType); + form.put("refresh_token", refreshToken); + form.put("timestamp", timestamp); + form.put("nonce", nonce); + String signStr = properties.getAppid() + "|" + properties.getSecretKey() + "|" + refreshToken + "|" + grantType + "|" + timestamp + "|" + nonce; + form.put("sign", DigestUtil.md5Hex(signStr)); + HttpResponse response = HttpRequest.get(buildUrl("/cgi-bin/bme-auth/auth/refresh_token")) + .form(form) + .timeout(properties.getTimeoutMs()) + .execute(); + return handleTokenResponse(response); + } + + private BmeToken handleTokenResponse(HttpResponse response) { + if (response.getStatus() != 200) { + throw new BusinessException("获取中台token失败,HTTP状态码:" + response.getStatus()); + } + JSONObject resp; + try { + resp = JSON.parseObject(response.body()); + } catch (Exception e) { + throw new BusinessException("解析中台token响应失败"); + } + if (resp == null || !"0".equals(resp.getString("code"))) { + String message = resp != null ? resp.getString("msg") : "未知错误"; + throw new BusinessException("获取中台token失败:" + message); + } + JSONObject data = resp.getJSONObject("data"); + if (data == null) { + throw new BusinessException("中台token响应缺少数据"); + } + String accessToken = data.getString("access_token"); + String refreshToken = data.getString("refresh_token"); + int expiresIn = data.getIntValue("expires_in"); + if (StrUtil.hasBlank(accessToken, refreshToken) || expiresIn <= 0) { + throw new BusinessException("中台token响应不完整"); + } + long expireAt = System.currentTimeMillis() / 1000 + expiresIn - TOKEN_EXPIRE_MARGIN_SECONDS; + BmeToken token = new BmeToken(); + token.setAccessToken(accessToken); + token.setRefreshToken(refreshToken); + token.setExpireAt(expireAt); + // refresh_token有效期5天,这里缓存5天 + redisUtil.set(TOKEN_CACHE_KEY, token, Duration.ofDays(5).getSeconds(), TimeUnit.SECONDS); + return token; + } + + private void ensureConfig() { + if (StrUtil.hasBlank(properties.getBaseUrl(), properties.getAppid(), properties.getSecretKey())) { + throw new BusinessException("排班接口配置缺失,请检查 led.bme 配置"); + } + } + + private String buildUrl(String path) { + String base = properties.getBaseUrl(); + if (StrUtil.isBlank(base)) { + throw new BusinessException("排班接口基础地址未配置"); + } + if (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + if (!path.startsWith("/")) { + path = "/" + path; + } + // 兼容 baseUrl 已包含 /cgi-bin 的场景,避免拼接成 /cgi-bin/cgi-bin + if (base.endsWith("/cgi-bin") && path.startsWith("/cgi-bin/")) { + path = path.substring("/cgi-bin".length()); + } + return base + path; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 751eff3..368b20f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -198,6 +198,17 @@ springdoc: swagger-ui: enabled: true +# LED - 排班接口(业务中台)对接配置 +led: + bme: + base-url: ${LED_BME_BASE_URL:http://16.1.4.201:7979} + appid: ${LED_BME_APPID:BQ73n58Lf} + secret-key: ${LED_BME_SECRET_KEY:jk720-DCPnGq@5t8} + mechanism-id: ${LED_BME_MECHANISM_ID:10001} + default-ext-user-id: ${LED_BME_DEFAULT_EXT_USER_ID:txzhyy} + default-hospital-id: ${LED_BME_DEFAULT_HOSPITAL_ID:} + timeout-ms: ${LED_BME_TIMEOUT_MS:10000} + # 启用 Knife4j knife4j: enable: true