Files
his/openhis-server-new/.agents/skills/spring-boot-engineer/references/web.md
chenqi 89bf85fd97 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
- 术后一站式结算功能待后续开发
2026-02-05 23:47:02 +08:00

296 lines
8.8 KiB
Markdown

# 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 |