- 数据库:在adm_charge_item表添加SourceBillNo字段 - 后端实体类:更新ChargeItem.java添加SourceBillNo字段 - 前端组件:创建手术计费界面(基于门诊划价界面) - 后端API:扩展PrePrePaymentDto支持手术计费标识 - 后端Service:扩展getChargeItems方法支持手术计费过滤 - 门诊手术安排界面:添加【计费】按钮 注意事项: - 需要手动执行SQL脚本:openhis-server-new/sql/add_source_bill_no_to_adm_charge_item.sql - 术后一站式结算功能待后续开发
12 KiB
12 KiB
Cloud Native - Spring Cloud
Spring Cloud Config Server
// Config Server
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
// application.yml
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/example/config-repo
default-label: main
search-paths: '{application}'
username: ${GIT_USERNAME}
password: ${GIT_PASSWORD}
native:
search-locations: classpath:/config
security:
user:
name: config-user
password: ${CONFIG_PASSWORD}
// Config Client
@SpringBootApplication
public class ClientApplication {
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
}
// application.yml (Config Client)
spring:
application:
name: user-service
config:
import: "configserver:http://localhost:8888"
cloud:
config:
username: config-user
password: ${CONFIG_PASSWORD}
fail-fast: true
retry:
max-attempts: 6
initial-interval: 1000
Dynamic Configuration Refresh
@RestController
@RefreshScope
public class ConfigController {
@Value("${app.feature.enabled:false}")
private boolean featureEnabled;
@Value("${app.max-connections:100}")
private int maxConnections;
@GetMapping("/config")
public Map<String, Object> getConfig() {
return Map.of(
"featureEnabled", featureEnabled,
"maxConnections", maxConnections
);
}
}
// Refresh configuration via Actuator endpoint:
// POST /actuator/refresh
Service Discovery - Eureka
// Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
// application.yml (Eureka Server)
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
// Eureka Client
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// application.yml (Eureka Client)
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
registry-fetch-interval-seconds: 5
instance:
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30
Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f
.rewritePath("/api/users/(?<segment>.*)", "/users/${segment}")
.addRequestHeader("X-Gateway", "Spring-Cloud-Gateway")
.circuitBreaker(config -> config
.setName("userServiceCircuitBreaker")
.setFallbackUri("forward:/fallback/users")
)
.retry(config -> config
.setRetries(3)
.setStatuses(HttpStatus.SERVICE_UNAVAILABLE)
)
)
.uri("lb://user-service")
)
.route("order-service", r -> r
.path("/api/orders/**")
.filters(f -> f
.rewritePath("/api/orders/(?<segment>.*)", "/orders/${segment}")
.requestRateLimiter(config -> config
.setRateLimiter(redisRateLimiter())
.setKeyResolver(userKeyResolver())
)
)
.uri("lb://order-service")
)
.build();
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(10, 20); // replenishRate, burstCapacity
}
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-User-Id")
);
}
}
// application.yml (Gateway)
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-methods:
- GET
- POST
- PUT
- DELETE
allowed-headers: "*"
Circuit Breaker - Resilience4j
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final WebClient webClient;
@CircuitBreaker(name = "externalApi", fallbackMethod = "getFallbackData")
@Retry(name = "externalApi")
@RateLimiter(name = "externalApi")
public Mono<ExternalData> getData(String id) {
return webClient
.get()
.uri("/data/{id}", id)
.retrieve()
.bodyToMono(ExternalData.class)
.timeout(Duration.ofSeconds(3));
}
private Mono<ExternalData> getFallbackData(String id, Exception e) {
log.warn("Fallback triggered for id: {}, error: {}", id, e.getMessage());
return Mono.just(new ExternalData(id, "Fallback data", LocalDateTime.now()));
}
}
// application.yml
resilience4j:
circuitbreaker:
instances:
externalApi:
register-health-indicator: true
sliding-window-size: 10
minimum-number-of-calls: 5
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
wait-duration-in-open-state: 5s
failure-rate-threshold: 50
event-consumer-buffer-size: 10
retry:
instances:
externalApi:
max-attempts: 3
wait-duration: 1s
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
ratelimiter:
instances:
externalApi:
limit-for-period: 10
limit-refresh-period: 1s
timeout-duration: 0s
Distributed Tracing - Micrometer Tracing
// application.yml
management:
tracing:
sampling:
probability: 1.0
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
// Custom spans
@Service
@RequiredArgsConstructor
public class OrderService {
private final Tracer tracer;
private final OrderRepository orderRepository;
public Order processOrder(OrderRequest request) {
Span span = tracer.nextSpan().name("processOrder").start();
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
span.tag("order.type", request.type());
span.tag("order.items", String.valueOf(request.items().size()));
// Business logic
Order order = createOrder(request);
span.event("order.created");
return order;
} finally {
span.end();
}
}
}
Load Balancing with Spring Cloud LoadBalancer
@Configuration
@LoadBalancerClient(name = "user-service", configuration = UserServiceLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}
@Configuration
public class UserServiceLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
LoadBalancerClientFactory clientFactory,
ObjectProvider<LoadBalancerProperties> properties) {
return new RandomLoadBalancer(
clientFactory.getLazyProvider("user-service", ServiceInstanceListSupplier.class),
"user-service"
);
}
}
@Service
@RequiredArgsConstructor
public class UserClientService {
private final WebClient.Builder webClientBuilder;
public Mono<User> getUser(Long id) {
return webClientBuilder
.baseUrl("http://user-service")
.build()
.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
}
Health Checks & Actuator
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
boolean serviceUp = checkExternalService();
if (serviceUp) {
return Health.up()
.withDetail("externalService", "Available")
.withDetail("timestamp", LocalDateTime.now())
.build();
} else {
return Health.down()
.withDetail("externalService", "Unavailable")
.withDetail("error", "Connection timeout")
.build();
}
}
private boolean checkExternalService() {
// Check external dependency
return true;
}
}
// application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
probes:
enabled: true
health:
livenessState:
enabled: true
readinessState:
enabled: true
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
Kubernetes Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Docker Configuration
# Dockerfile (Multi-stage)
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.Application"]
Quick Reference
| Component | Purpose |
|---|---|
| Config Server | Centralized configuration management |
| Eureka | Service discovery and registration |
| Gateway | API gateway with routing, filtering, load balancing |
| Circuit Breaker | Fault tolerance and fallback patterns |
| Load Balancer | Client-side load balancing |
| Tracing | Distributed tracing across services |
| Actuator | Production-ready monitoring and management |
| Kubernetes | Container orchestration and deployment |