feat: 结果状态接口切换 getResultStatus、全局异常分级细化、日期序列化对齐、Oracle NLS/orai18n 与 BSM 结果视图改造、API 访问日志过滤器
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,12 @@
|
|||||||
<artifactId>ojdbc11</artifactId>
|
<artifactId>ojdbc11</artifactId>
|
||||||
<version>${ojdbc.version}</version>
|
<version>${ojdbc.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Oracle NLS 字符集支持:ZHS16GBK 等中文/多字节字符集转换表,缺失报 ORA-17056 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.oracle.database.nls</groupId>
|
||||||
|
<artifactId>orai18n</artifactId>
|
||||||
|
<version>${ojdbc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- SqlServer -->
|
<!-- SqlServer -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ public class OrderApi {
|
|||||||
@Mapping("/getResultStatus/{peId}")
|
@Mapping("/getResultStatus/{peId}")
|
||||||
public Result<List<MedicalMiddleResultVo>> getResultStatus(Context ctx, @Path("peId") String peId) {
|
public Result<List<MedicalMiddleResultVo>> getResultStatus(Context ctx, @Path("peId") String peId) {
|
||||||
checkBasic(ctx);
|
checkBasic(ctx);
|
||||||
return Result.success(service.getResultByPeId(peId));
|
return Result.success(service.getResultStatus(peId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 17. 根据身份证号查询当前年的 peId 和预约日期 */
|
/** 17. 根据身份证号查询当前年的 peId 和预约日期 */
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.hospital.front.infra;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.Filter;
|
||||||
|
import org.noear.solon.core.handle.FilterChain;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局接口访问日志过滤器。
|
||||||
|
*
|
||||||
|
* 统一打印每个请求的调用日志:HTTP 方法、路径、来源 IP、请求参数、耗时、响应状态码。
|
||||||
|
* 覆盖所有 Controller(/order、/api/admin),新增接口自动生效,无需逐个手写日志。
|
||||||
|
*
|
||||||
|
* 参数覆盖说明:
|
||||||
|
* - @Param(query/form)与 @Path(路径变量)统一通过 ctx.paramMap() 取;@Path 的值同时也在 ctx.path() 里可见。
|
||||||
|
* - @Body 参数(saveOrder/addUnit)不在此读取,避免消费 body 流破坏 @Body 反序列化;
|
||||||
|
* 由 service 层日志覆盖(saveOrder 打印 peInfoStr/peItemStr,addUnit 打印 units)。
|
||||||
|
*
|
||||||
|
* 顺序:index = -1 保证本过滤器在 GlobalExceptionHandler / IpWhitelistFilter 之前执行(最外层),
|
||||||
|
* 从而能记录到异常被处理后写入 ctx 的最终状态码(401/500 等)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component(index = -1)
|
||||||
|
public class ApiAccessLogFilter implements Filter {
|
||||||
|
|
||||||
|
/** 无需打印访问日志的路径(高频健康探测等,避免刷屏) */
|
||||||
|
private static final Set<String> SKIP_LOG_PATHS = Set.of("/api/admin/health");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
chain.doFilter(ctx);
|
||||||
|
} finally {
|
||||||
|
long cost = System.currentTimeMillis() - start;
|
||||||
|
if (!SKIP_LOG_PATHS.contains(ctx.path())) {
|
||||||
|
log.info("[接口调用] {} {} ip={} 参数=[{}] cost={}ms status={}",
|
||||||
|
ctx.method(), ctx.path(), ctx.remoteIp(), paramsOf(ctx), cost, ctx.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总请求参数(query/form/path,@Body 除外),形如 key1=v1, key2=v2 */
|
||||||
|
private String paramsOf(Context ctx) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (var kv : ctx.paramMap()) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
sb.append(kv.getKey()).append('=').append(kv.getFirstValue());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,9 +24,21 @@ public class GlobalExceptionHandler implements Filter {
|
|||||||
try {
|
try {
|
||||||
chain.doFilter(ctx);
|
chain.doFilter(ctx);
|
||||||
} catch (org.noear.solon.core.exception.StatusException e) {
|
} catch (org.noear.solon.core.exception.StatusException e) {
|
||||||
// 404/405 等路由状态异常:多为扫描器探测,INFO 一行即可(不打堆栈,避免刷爆日志)
|
int code = e.getCode();
|
||||||
log.info("路由未匹配:{} {}(HTTP {})", ctx.method(), ctx.path(), e.getCode());
|
if (code >= 500) {
|
||||||
renderStatus(ctx, e.getCode());
|
// 5xx 服务端状态异常:真实故障,ERROR 记堆栈
|
||||||
|
log.error("服务端状态异常:{} {}(HTTP {})", ctx.method(), ctx.path(), code, e);
|
||||||
|
} else if (code == 400) {
|
||||||
|
// 400 请求解析失败(多为 multipart/Content-Type 错误):真实调用失败,WARN 并带 Content-Type 便于定位
|
||||||
|
log.warn("请求解析失败(HTTP 400):{} {},Content-Type={}", ctx.method(), ctx.path(), ctx.header("Content-Type"));
|
||||||
|
} else if (code == 404) {
|
||||||
|
// 404 路由未匹配:多为扫描器探测,DEBUG 静默(INFO 级别下不再输出)
|
||||||
|
log.debug("路由未匹配:{} {}(HTTP 404)", ctx.method(), ctx.path());
|
||||||
|
} else {
|
||||||
|
// 其它 4xx(401/403/405/415 等)
|
||||||
|
log.info("客户端状态异常:{} {}(HTTP {})", ctx.method(), ctx.path(), code);
|
||||||
|
}
|
||||||
|
renderStatus(ctx, code);
|
||||||
} catch (BusinessException e) {
|
} catch (BusinessException e) {
|
||||||
String msg = e.getMessage() != null ? e.getMessage() : "业务异常";
|
String msg = e.getMessage() != null ? e.getMessage() : "业务异常";
|
||||||
log.warn("业务异常:{}", e.getMessage());
|
log.warn("业务异常:{}", e.getMessage());
|
||||||
@@ -59,7 +71,11 @@ public class GlobalExceptionHandler implements Filter {
|
|||||||
* 输出 Result JSON 响应(与原 hospitalmiddle 契约一致:{"success":false,"msg":...})。
|
* 输出 Result JSON 响应(与原 hospitalmiddle 契约一致:{"success":false,"msg":...})。
|
||||||
*/
|
*/
|
||||||
private void renderResult(Context ctx, Result<?> result) {
|
private void renderResult(Context ctx, Result<?> result) {
|
||||||
ctx.status(200);
|
// 保留上游预设的 4xx/5xx 状态码(如 checkBasic 认证失败已设 401),否则默认 200。
|
||||||
|
// 若强行刷成 200,Basic 认证失败将不再返回 401,浏览器不会弹出账号密码框。
|
||||||
|
if (ctx.status() < 400) {
|
||||||
|
ctx.status(200);
|
||||||
|
}
|
||||||
ctx.contentType("application/json;charset=UTF-8");
|
ctx.contentType("application/json;charset=UTF-8");
|
||||||
ctx.output("{\"success\":" + result.isSuccess()
|
ctx.output("{\"success\":" + result.isSuccess()
|
||||||
+ ",\"msg\":" + toJsonValue(result.getMsg())
|
+ ",\"msg\":" + toJsonValue(result.getMsg())
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ solon:
|
|||||||
app:
|
app:
|
||||||
name: "hospital-front"
|
name: "hospital-front"
|
||||||
group: "hospital"
|
group: "hospital"
|
||||||
# JSON 序列化对齐原 hospitalmiddle(Jackson 默认保留 null 字段)
|
# JSON 序列化对齐原 hospitalmiddle(Jackson 默认保留 null 字段 + 日期格式)
|
||||||
serialization:
|
serialization:
|
||||||
json:
|
json:
|
||||||
nullAsWriteable: true
|
nullAsWriteable: true
|
||||||
|
# 对齐原 spring.jackson.date-format / time-zone:Date/Timestamp 序列化为字符串而非毫秒时间戳
|
||||||
|
dateAsFormat: "yyyy-MM-dd HH:mm:ss"
|
||||||
|
dateAsTimeZone: "GMT+8"
|
||||||
|
|
||||||
# 应用配置
|
# 应用配置
|
||||||
# 医院注册清单由代码决定(各 Adapter 的 @Component),此处仅指定激活哪家
|
# 医院注册清单由代码决定(各 Adapter 的 @Component),此处仅指定激活哪家
|
||||||
|
|||||||
@@ -338,17 +338,7 @@
|
|||||||
ID_NO AS "idNo",
|
ID_NO AS "idNo",
|
||||||
NAME AS "name",
|
NAME AS "name",
|
||||||
TO_CHAR(audit_date, 'YYYY-MM-DD') AS "auditDate"
|
TO_CHAR(audit_date, 'YYYY-MM-DD') AS "auditDate"
|
||||||
FROM (
|
FROM V_PHYEXAM_RESULT
|
||||||
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.name, b.unit_code,
|
|
||||||
(SELECT t.unit_name FROM pe_unit_dict t WHERE t.unit_code = b.unit_code) unit_name,
|
|
||||||
b.pe_queue_date, b.audit_date,
|
|
||||||
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
|
||||||
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
|
||||||
FROM pe_master_index a, pe_visit b, pe_result_dict c
|
|
||||||
WHERE a.pe_id = b.pe_id
|
|
||||||
AND b.pe_id = c.pe_id
|
|
||||||
AND b.pe_visit_id = c.pe_visit_id
|
|
||||||
)
|
|
||||||
<where>
|
<where>
|
||||||
<choose>
|
<choose>
|
||||||
<when test="auditDate != null and auditDate != ''">
|
<when test="auditDate != null and auditDate != ''">
|
||||||
|
|||||||
Reference in New Issue
Block a user