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,381 @@
|
||||
# Data Access - Spring Data JPA
|
||||
|
||||
## JPA Entity Pattern
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "users", indexes = {
|
||||
@Index(name = "idx_email", columnList = "email", unique = true),
|
||||
@Index(name = "idx_username", columnList = "username")
|
||||
})
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Getter @Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<Address> addresses = new ArrayList<>();
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "user_roles",
|
||||
joinColumns = @JoinColumn(name = "user_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||||
)
|
||||
@Builder.Default
|
||||
private Set<Role> roles = new HashSet<>();
|
||||
|
||||
@CreatedDate
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Version
|
||||
private Long version;
|
||||
|
||||
// Helper methods for bidirectional relationships
|
||||
public void addAddress(Address address) {
|
||||
addresses.add(address);
|
||||
address.setUser(this);
|
||||
}
|
||||
|
||||
public void removeAddress(Address address) {
|
||||
addresses.remove(address);
|
||||
address.setUser(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Spring Data JPA Repository
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long>,
|
||||
JpaSpecificationExecutor<User> {
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email = :email")
|
||||
Optional<User> findByEmailWithRoles(@Param("email") String email);
|
||||
|
||||
@Query("SELECT u FROM User u WHERE u.active = true AND u.createdAt >= :since")
|
||||
List<User> findActiveUsersSince(@Param("since") LocalDateTime since);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :threshold")
|
||||
int deactivateInactiveUsers(@Param("threshold") LocalDateTime threshold);
|
||||
|
||||
// Projection for read-only DTOs
|
||||
@Query("SELECT new com.example.dto.UserSummary(u.id, u.username, u.email) " +
|
||||
"FROM User u WHERE u.active = true")
|
||||
List<UserSummary> findAllActiveSummaries();
|
||||
}
|
||||
```
|
||||
|
||||
## Repository with Specifications
|
||||
|
||||
```java
|
||||
public class UserSpecifications {
|
||||
|
||||
public static Specification<User> hasEmail(String email) {
|
||||
return (root, query, cb) ->
|
||||
email == null ? null : cb.equal(root.get("email"), email);
|
||||
}
|
||||
|
||||
public static Specification<User> isActive() {
|
||||
return (root, query, cb) -> cb.isTrue(root.get("active"));
|
||||
}
|
||||
|
||||
public static Specification<User> createdAfter(LocalDateTime date) {
|
||||
return (root, query, cb) ->
|
||||
date == null ? null : cb.greaterThanOrEqualTo(root.get("createdAt"), date);
|
||||
}
|
||||
|
||||
public static Specification<User> hasRole(String roleName) {
|
||||
return (root, query, cb) -> {
|
||||
Join<User, Role> roles = root.join("roles", JoinType.INNER);
|
||||
return cb.equal(roles.get("name"), roleName);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in service
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
|
||||
Specification<User> spec = Specification
|
||||
.where(UserSpecifications.hasEmail(criteria.email()))
|
||||
.and(UserSpecifications.isActive())
|
||||
.and(UserSpecifications.createdAfter(criteria.createdAfter()));
|
||||
|
||||
return userRepository.findAll(spec, pageable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Transaction Management
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class OrderService {
|
||||
private final OrderRepository orderRepository;
|
||||
private final PaymentService paymentService;
|
||||
private final InventoryService inventoryService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
@Transactional
|
||||
public Order createOrder(OrderCreateRequest request) {
|
||||
// All operations in single transaction
|
||||
Order order = Order.builder()
|
||||
.customerId(request.customerId())
|
||||
.status(OrderStatus.PENDING)
|
||||
.build();
|
||||
|
||||
request.items().forEach(item -> {
|
||||
inventoryService.reserveStock(item.productId(), item.quantity());
|
||||
order.addItem(item);
|
||||
});
|
||||
|
||||
order = orderRepository.save(order);
|
||||
|
||||
try {
|
||||
paymentService.processPayment(order);
|
||||
order.setStatus(OrderStatus.PAID);
|
||||
} catch (PaymentException e) {
|
||||
order.setStatus(OrderStatus.PAYMENT_FAILED);
|
||||
throw e; // Transaction will rollback
|
||||
}
|
||||
|
||||
return orderRepository.save(order);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logOrderEvent(Long orderId, String event) {
|
||||
// Separate transaction - will commit even if parent rolls back
|
||||
OrderEvent orderEvent = new OrderEvent(orderId, event);
|
||||
orderEventRepository.save(orderEvent);
|
||||
}
|
||||
|
||||
@Transactional(noRollbackFor = NotificationException.class)
|
||||
public void completeOrder(Long orderId) {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
|
||||
|
||||
order.setStatus(OrderStatus.COMPLETED);
|
||||
orderRepository.save(order);
|
||||
|
||||
// Won't rollback transaction if notification fails
|
||||
try {
|
||||
notificationService.sendCompletionEmail(order);
|
||||
} catch (NotificationException e) {
|
||||
log.error("Failed to send notification for order {}", orderId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Auditing Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaAuditingConfig {
|
||||
|
||||
@Bean
|
||||
public AuditorAware<String> auditorProvider() {
|
||||
return () -> {
|
||||
Authentication authentication = SecurityContextHolder
|
||||
.getContext()
|
||||
.getAuthentication();
|
||||
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return Optional.of("system");
|
||||
}
|
||||
|
||||
return Optional.of(authentication.getName());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Getter @Setter
|
||||
public abstract class AuditableEntity {
|
||||
|
||||
@CreatedDate
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@CreatedBy
|
||||
@Column(nullable = false, updatable = false, length = 100)
|
||||
private String createdBy;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(nullable = false, length = 100)
|
||||
private String updatedBy;
|
||||
}
|
||||
```
|
||||
|
||||
## Projections
|
||||
|
||||
```java
|
||||
// Interface-based projection
|
||||
public interface UserSummary {
|
||||
Long getId();
|
||||
String getUsername();
|
||||
String getEmail();
|
||||
|
||||
@Value("#{target.firstName + ' ' + target.lastName}")
|
||||
String getFullName();
|
||||
}
|
||||
|
||||
// Class-based projection (DTO)
|
||||
public record UserSummaryDto(
|
||||
Long id,
|
||||
String username,
|
||||
String email
|
||||
) {}
|
||||
|
||||
// Usage
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
List<UserSummary> findAllBy();
|
||||
|
||||
<T> List<T> findAllBy(Class<T> type);
|
||||
}
|
||||
|
||||
// Service usage
|
||||
List<UserSummary> summaries = userRepository.findAllBy();
|
||||
List<UserSummaryDto> dtos = userRepository.findAllBy(UserSummaryDto.class);
|
||||
```
|
||||
|
||||
## Query Optimization
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class UserQueryService {
|
||||
private final UserRepository userRepository;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
// N+1 problem solved with JOIN FETCH
|
||||
@Query("SELECT DISTINCT u FROM User u " +
|
||||
"LEFT JOIN FETCH u.addresses " +
|
||||
"LEFT JOIN FETCH u.roles " +
|
||||
"WHERE u.active = true")
|
||||
List<User> findAllActiveWithAssociations();
|
||||
|
||||
// Batch fetching
|
||||
@BatchSize(size = 25)
|
||||
@OneToMany(mappedBy = "user")
|
||||
private List<Order> orders;
|
||||
|
||||
// EntityGraph for dynamic fetching
|
||||
@EntityGraph(attributePaths = {"addresses", "roles"})
|
||||
List<User> findAllByActiveTrue();
|
||||
|
||||
// Pagination to avoid loading all data
|
||||
public Page<User> findAllUsers(Pageable pageable) {
|
||||
return userRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// Native query for complex queries
|
||||
@Query(value = """
|
||||
SELECT u.* FROM users u
|
||||
INNER JOIN orders o ON u.id = o.user_id
|
||||
WHERE o.created_at >= :since
|
||||
GROUP BY u.id
|
||||
HAVING COUNT(o.id) >= :minOrders
|
||||
""", nativeQuery = true)
|
||||
List<User> findFrequentBuyers(@Param("since") LocalDateTime since,
|
||||
@Param("minOrders") int minOrders);
|
||||
}
|
||||
```
|
||||
|
||||
## Database Migrations (Flyway)
|
||||
|
||||
```sql
|
||||
-- V1__create_users_table.sql
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(100) NOT NULL,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
version BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_active ON users(active);
|
||||
|
||||
-- V2__create_addresses_table.sql
|
||||
CREATE TABLE addresses (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
street VARCHAR(200) NOT NULL,
|
||||
city VARCHAR(100) NOT NULL,
|
||||
country VARCHAR(2) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_addresses_user_id ON addresses(user_id);
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Annotation | Purpose |
|
||||
|------------|---------|
|
||||
| `@Entity` | Marks class as JPA entity |
|
||||
| `@Table` | Specifies table details and indexes |
|
||||
| `@Id` | Marks primary key field |
|
||||
| `@GeneratedValue` | Auto-generated primary key strategy |
|
||||
| `@Column` | Column constraints and mapping |
|
||||
| `@OneToMany/@ManyToOne` | One-to-many/many-to-one relationships |
|
||||
| `@ManyToMany` | Many-to-many relationships |
|
||||
| `@JoinColumn/@JoinTable` | Join column/table configuration |
|
||||
| `@Transactional` | Declares transaction boundaries |
|
||||
| `@Query` | Custom JPQL/native queries |
|
||||
| `@Modifying` | Marks query as UPDATE/DELETE |
|
||||
| `@EntityGraph` | Defines fetch graph for associations |
|
||||
| `@Version` | Optimistic locking version field |
|
||||
Reference in New Issue
Block a user