feat(credit): 新增企业管理功能模块

- 新增企业实体类CreditCompany,包含企业基本信息字段
- 新增企业控制器CreditCompanyController,提供CRUD接口
- 新增企业导入参数类CreditCompanyImportParam,支持Excel导入
- 新增企业查询参数类CreditCompanyParam,支持条件查询
- 新增企业Mapper接口及XML映射文件,实现关联查询
- 实现企业分页查询、列表查询、详情查询接口
- 实现企业新增、修改、删除接口
- 实现企业批量导入功能,支持Excel模板下载
- 实现企业数据校验和重复数据处理逻辑
- 添加企业导入模板下载接口
- 支持企业信息的完整字段映射和转换逻辑
- 添加企业查询条件注解,支持动态SQL查询
- 实现企业关联查询SQL,支持多字段模糊匹配
- 添加企业操作日志记录和权限控制注解
- 完善企业导入异常处理和错误信息收集机制
This commit is contained in:
2025-12-17 14:48:13 +08:00
parent 57cdb72208
commit 6affaba5c3
8 changed files with 498 additions and 0 deletions

View File

@@ -84,6 +84,8 @@
</if> </if>
<if test="param.keywords != null"> <if test="param.keywords != null">
AND (a.name LIKE CONCAT('%', #{param.keywords}, '%') AND (a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.procurement_name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.winning_name LIKE CONCAT('%', #{param.keywords}, '%')
) )
</if> </if>
</where> </where>

View File

@@ -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;
}

View File

@@ -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<JSONObject> searchStopAndReplace(@RequestBody BmeStopReplaceParam param) {
return success(ledScheduleService.searchStopAndReplace(param));
}
@Operation(summary = "查询医生排班信息当日剩余号源10018")
@PostMapping("/number-sources")
public ApiResult<JSONObject> searchNumberSources(@RequestBody BmeNumberSourcesParam param) {
return success(ledScheduleService.searchNumberSources(param));
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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;
}
}

View File

@@ -198,6 +198,17 @@ springdoc:
swagger-ui: swagger-ui:
enabled: true 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
knife4j: knife4j:
enable: true enable: true