feat: 门诊手术中计费功能
- 数据库:在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 - 术后一站式结算功能待后续开发
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# Web Layer - Controllers & REST APIs
|
||||
|
||||
## REST Controller Pattern
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/users")
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<UserResponse>> getUsers(
|
||||
@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
|
||||
Page<UserResponse> users = userService.findAll(pageable);
|
||||
return ResponseEntity.ok(users);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
|
||||
UserResponse user = userService.findById(id);
|
||||
return ResponseEntity.ok(user);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserResponse> createUser(
|
||||
@Valid @RequestBody UserCreateRequest request) {
|
||||
UserResponse user = userService.create(request);
|
||||
URI location = ServletUriComponentsBuilder
|
||||
.fromCurrentRequest()
|
||||
.path("/{id}")
|
||||
.buildAndExpand(user.id())
|
||||
.toUri();
|
||||
return ResponseEntity.created(location).body(user);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> updateUser(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody UserUpdateRequest request) {
|
||||
UserResponse user = userService.update(id, request);
|
||||
return ResponseEntity.ok(user);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void deleteUser(@PathVariable Long id) {
|
||||
userService.delete(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Request DTOs with Validation
|
||||
|
||||
```java
|
||||
public record UserCreateRequest(
|
||||
@NotBlank(message = "Email is required")
|
||||
@Email(message = "Email must be valid")
|
||||
String email,
|
||||
|
||||
@NotBlank(message = "Password is required")
|
||||
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
|
||||
@Pattern(regexp = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).*$",
|
||||
message = "Password must contain uppercase, lowercase, and digit")
|
||||
String password,
|
||||
|
||||
@NotBlank(message = "Username is required")
|
||||
@Size(min = 3, max = 50)
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Username must be alphanumeric")
|
||||
String username,
|
||||
|
||||
@Min(value = 18, message = "Must be at least 18")
|
||||
@Max(value = 120, message = "Must be at most 120")
|
||||
Integer age
|
||||
) {}
|
||||
|
||||
public record UserUpdateRequest(
|
||||
@Email(message = "Email must be valid")
|
||||
String email,
|
||||
|
||||
@Size(min = 3, max = 50)
|
||||
String username
|
||||
) {}
|
||||
```
|
||||
|
||||
## Response DTOs
|
||||
|
||||
```java
|
||||
public record UserResponse(
|
||||
Long id,
|
||||
String email,
|
||||
String username,
|
||||
Integer age,
|
||||
Boolean active,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
public static UserResponse from(User user) {
|
||||
return new UserResponse(
|
||||
user.getId(),
|
||||
user.getEmail(),
|
||||
user.getUsername(),
|
||||
user.getAge(),
|
||||
user.getActive(),
|
||||
user.getCreatedAt(),
|
||||
user.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global Exception Handling
|
||||
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ResourceNotFoundException.class)
|
||||
public ResponseEntity<ErrorResponse> handleNotFound(
|
||||
ResourceNotFoundException ex, WebRequest request) {
|
||||
log.error("Resource not found: {}", ex.getMessage());
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
HttpStatus.NOT_FOUND.value(),
|
||||
ex.getMessage(),
|
||||
request.getDescription(false),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ValidationErrorResponse> handleValidation(
|
||||
MethodArgumentNotValidException ex) {
|
||||
Map<String, String> errors = ex.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
FieldError::getField,
|
||||
error -> error.getDefaultMessage() != null
|
||||
? error.getDefaultMessage()
|
||||
: "Invalid value"
|
||||
));
|
||||
|
||||
ValidationErrorResponse response = new ValidationErrorResponse(
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"Validation failed",
|
||||
errors,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public ResponseEntity<ErrorResponse> handleDataIntegrity(
|
||||
DataIntegrityViolationException ex, WebRequest request) {
|
||||
log.error("Data integrity violation", ex);
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
HttpStatus.CONFLICT.value(),
|
||||
"Data integrity violation - resource may already exist",
|
||||
request.getDescription(false),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGlobalException(
|
||||
Exception ex, WebRequest request) {
|
||||
log.error("Unexpected error", ex);
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"An unexpected error occurred",
|
||||
request.getDescription(false),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
record ErrorResponse(
|
||||
int status,
|
||||
String message,
|
||||
String path,
|
||||
LocalDateTime timestamp
|
||||
) {}
|
||||
|
||||
record ValidationErrorResponse(
|
||||
int status,
|
||||
String message,
|
||||
Map<String, String> errors,
|
||||
LocalDateTime timestamp
|
||||
) {}
|
||||
```
|
||||
|
||||
## Custom Validation
|
||||
|
||||
```java
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = UniqueEmailValidator.class)
|
||||
public @interface UniqueEmail {
|
||||
String message() default "Email already exists";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
public boolean isValid(String email, ConstraintValidatorContext context) {
|
||||
if (email == null) return true;
|
||||
return !userRepository.existsByEmail(email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebClient for External APIs
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
@Bean
|
||||
public WebClient webClient(WebClient.Builder builder) {
|
||||
return builder
|
||||
.baseUrl("https://api.example.com")
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.filter(logRequest())
|
||||
.build();
|
||||
}
|
||||
|
||||
private ExchangeFilterFunction logRequest() {
|
||||
return ExchangeFilterFunction.ofRequestProcessor(request -> {
|
||||
log.info("Request: {} {}", request.method(), request.url());
|
||||
return Mono.just(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalApiService {
|
||||
private final WebClient webClient;
|
||||
|
||||
public Mono<ExternalDataResponse> fetchData(String id) {
|
||||
return webClient
|
||||
.get()
|
||||
.uri("/data/{id}", id)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, response ->
|
||||
Mono.error(new ResourceNotFoundException("External resource not found")))
|
||||
.onStatus(HttpStatusCode::is5xxServerError, response ->
|
||||
Mono.error(new ServiceUnavailableException("External service unavailable")))
|
||||
.bodyToMono(ExternalDataResponse.class)
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.retry(3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("http://localhost:3000", "https://example.com")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Annotation | Purpose |
|
||||
|------------|---------|
|
||||
| `@RestController` | Marks class as REST controller (combines @Controller + @ResponseBody) |
|
||||
| `@RequestMapping` | Maps HTTP requests to handler methods |
|
||||
| `@GetMapping/@PostMapping` | HTTP method-specific mappings |
|
||||
| `@PathVariable` | Extracts values from URI path |
|
||||
| `@RequestParam` | Extracts query parameters |
|
||||
| `@RequestBody` | Binds request body to method parameter |
|
||||
| `@Valid` | Triggers validation on request body |
|
||||
| `@RestControllerAdvice` | Global exception handling for REST controllers |
|
||||
| `@ResponseStatus` | Sets HTTP status code for method |
|
||||
Reference in New Issue
Block a user