- 数据库:在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 - 术后一站式结算功能待后续开发
499 lines
12 KiB
Markdown
499 lines
12 KiB
Markdown
# Cloud Native - Spring Cloud
|
|
|
|
## Spring Cloud Config Server
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```yaml
|
|
# 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
|
|
# 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 |
|