Напреден развој на апликацијата
Трансакции
executeTrade (точка за влез)
Опис: Методот executeTrade во PortfolioService е централната точка за влез која ја повикува TradeRequestController при одобрување на барање за тргување. Врши рутирање кон buyStock или sellStock врз основа на типот на трансакцијата.
Целата операција е @Transactional - доколку кој било чекор не успее, се откажува целосно.
@Transactional
public void executeTrade(TradeRequest tr) {
BigDecimal price = BigDecimal.valueOf(tr.getPricePerUnit());
Portfolio portfolio = tr.getPortfolio();
if ("BUY".equalsIgnoreCase(tr.getType())) {
buyStock(portfolio.getId(), tr.getStockSymbol(), tr.getQuantity(), price);
} else if ("SELL".equalsIgnoreCase(tr.getType())) {
sellStock(portfolio.getId(), tr.getStockSymbol(), tr.getQuantity(), price);
} else {
throw new RuntimeException("Unknown trade type: " + tr.getType());
}
}
Купување на акција (buyStock)
Опис: Методот buyStock во PortfolioService извршува повеќе зависни операции:
- Проверка дека портфолиото има доволно баланс за купување
- Намалување на балансот на портфолиото за вкупната вредност на купените акции
- Ажурирање на постоечко холдинг (или креирање на нов) со новата количина и пресметана weighted average цена
- Зачувување на трансакцискиот запис
Доколку кој било чекор не успее (на пр. акцијата не постои, портфолиото не е пронајдено, недоволен баланс), целата операција се откажува и балансот останува непроменет.
@Transactional
public void buyStock(Long portfolioId, String stockSymbol, int quantity, BigDecimal pricePerUnit) {
Portfolio portfolio = portfolioRepository.findById(portfolioId)
.orElseThrow(() -> new RuntimeException("Portfolio not found"));
BigDecimal totalCost = pricePerUnit.multiply(BigDecimal.valueOf(quantity));
if (portfolio.getBalance().compareTo(totalCost) < 0) {
throw new RuntimeException("Insufficient balance to buy stock");
}
portfolio.setBalance(portfolio.getBalance().subtract(totalCost));
Stock stock = stockRepository.findBySymbol(stockSymbol)
.orElseThrow(() -> new RuntimeException("Stock not found: " + stockSymbol));
PortfolioHolding holding = holdingRepository
.findByPortfolioIdAndStock_Symbol(portfolioId, stockSymbol)
.orElse(PortfolioHolding.builder()
.portfolio(portfolio)
.stock(stock)
.quantity(0)
.avgPrice(BigDecimal.ZERO)
.build());
// Recalculate weighted average price
BigDecimal currentTotalValue = holding.getAvgPrice().multiply(BigDecimal.valueOf(holding.getQuantity()));
BigDecimal newTotalValue = currentTotalValue.add(totalCost);
int newQuantity = holding.getQuantity() + quantity;
BigDecimal newAvgPrice = newTotalValue.divide(BigDecimal.valueOf(newQuantity), RoundingMode.HALF_UP);
holding.setQuantity(newQuantity);
holding.setAvgPrice(newAvgPrice);
holdingRepository.save(holding);
portfolioRepository.save(portfolio);
// Save transaction
Transaction transaction = new Transaction();
transaction.setUser(portfolio.getUser());
transaction.setStock(stock);
transaction.setType("BUY");
transaction.setQuantity(quantity);
transaction.setPrice(pricePerUnit.doubleValue());
transaction.setTimestamp(LocalDateTime.now());
transactionRepository.save(transaction);
}
Продавање на акција (sellStock)
Опис: Методот sellStock во PortfolioService:
- Проверка дека корисникот поседува доволно акции
- Намалување на количината во холдингот (или бришење ако достигне 0)
- Зголемување на балансот за приходот од продажбата
- Зачувување на трансакцискиот запис
@Transactional
public void sellStock(Long portfolioId, String stockSymbol, int quantity, BigDecimal pricePerUnit) {
Portfolio portfolio = portfolioRepository.findById(portfolioId)
.orElseThrow(() -> new RuntimeException("Portfolio not found"));
PortfolioHolding holding = holdingRepository
.findByPortfolioIdAndStock_Symbol(portfolioId, stockSymbol)
.orElseThrow(() -> new RuntimeException("Stock not found in portfolio"));
if (holding.getQuantity() < quantity) {
throw new RuntimeException("Not enough shares to sell");
}
if (pricePerUnit == null) {
// fallback: get latest price from stock
Stock stock = stockRepository.findBySymbol(stockSymbol)
.orElseThrow(() -> new RuntimeException("Stock not found: " + stockSymbol));
pricePerUnit = BigDecimal.valueOf(stock.getCurrentPrice());
}
// gain from the sale made
BigDecimal totalGain = pricePerUnit.multiply(BigDecimal.valueOf(quantity));
holding.setQuantity(holding.getQuantity() - quantity);
// if holding reaches zero - remove it from database
if (holding.getQuantity() == 0) {
holdingRepository.delete(holding);
} else {
holdingRepository.save(holding);
}
portfolio.setBalance(portfolio.getBalance().add(totalGain));
portfolioRepository.save(portfolio);
// Save transaction
Stock stock = stockRepository.findBySymbol(stockSymbol)
.orElseThrow(() -> new RuntimeException("Stock not found: " + stockSymbol));
Transaction transaction = new Transaction();
transaction.setUser(portfolio.getUser());
transaction.setStock(stock);
transaction.setType("SELL");
transaction.setQuantity(quantity);
transaction.setPrice(pricePerUnit.doubleValue());
transaction.setTimestamp(LocalDateTime.now());
transactionRepository.save(transaction);
}
Поврзување на Google OAuth акаунт (confirmLink)
Опис: Методот confirmLink во AuthLinkController поврзува Google OAuth акаунт со постоечки интерен акаунт. Операцијата вклучува:
- Верификација и читање на pending OAuth токенот
- Автентикација на интерните credentials на корисникот
- Додавање на
GOOGLEпровајдер во множеството на auth провајдери на корисникот - Зачувување на корисникот
- Бришење на pending токенот
@PostMapping("/confirm")
@Transactional
public ResponseEntity<?> confirmLink(@RequestBody Map<String, String> body) {
String pendingToken = body.get("pendingToken");
String usernameOrEmail = body.get("username");
String password = body.get("password");
if (pendingToken == null || usernameOrEmail == null || password == null) {
return ResponseEntity.badRequest().body("missing fields");
}
PendingLink pending = pendingLinkRepository.findByToken(pendingToken).orElse(null);
if (pending == null || pending.getExpiresAt().isBefore(Instant.now())) {
return ResponseEntity.status(410).body("pending token invalid or expired");
}
// auth internal credentials
try {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(usernameOrEmail, password)
);
} catch (Exception ex) {
return ResponseEntity.status(401).body("Invalid internal credentials");
}
// load user
User user = userRepository.findByUsername(usernameOrEmail)
.orElseGet(() -> userRepository.findByEmail(usernameOrEmail).orElse(null));
if (user == null) {
return ResponseEntity.status(404).body("User not found");
}
// verufy
// if (!user.getEmail().equalsIgnoreCase(pending.getEmail())) {
// return ResponseEntity.status(403).body("Pending link does not match authenticated user");
// }
if (!pending.getUser().getId().equals(user.getId())) {
return ResponseEntity.status(403).body("Pending link does not belong to user");
}
Set<AuthProvider> providers = user.getAuthProviders();
providers.add(AuthProvider.GOOGLE);
user.setAuthProviders(providers);
userRepository.save(user);
// delete pending
pendingLinkRepository.deleteByToken(pendingToken);
// generate token
String jwt = userService.generateToken(user);
Map<String, Object> resp = new HashMap<>();
resp.put("token", jwt);
resp.put("message", "Google account linked and logged in");
return ResponseEntity.ok(resp);
}
Database Connection Pooling
Spring Boot автоматски го вклучува HikariCP connection pool-от.
Конфигурација
Тековната конфигурација во application.yml го користи HikariCP со стандардните вредности:
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:9999/db_202526z_va_prj_tradingmk
username: db_202526z_va_prj_tradingmk_owner
password: ••••••••
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
Пример на користење на конекција од pool-от
Секој повик кон repository методите автоматски зема конекција од HikariCP pool-от и ја враќа по завршување. Следниот пример го прикажува текот при buyStock:
// При повик на buyStock(), HikariCP управува со конекциите:
@Transactional // <-- Spring отвора трансакција, HikariCP дава конекција
public void buyStock(...) {
portfolioRepository.findById(...) // конекција од pool-от се користи
stockRepository.findBySymbol(...) // истата конекција
holdingRepository.save(...) // истата конекција
portfolioRepository.save(...) // истата конекција
transactionRepository.save(...) // истата конекција
} // <-- COMMIT, конекцијата се враќа во pool-от
Без connection pooling, секој од овие повици би отворал и затворал посебна TCP конекција кон PostgreSQL - операција која трае ~50-100ms. Со HikariCP, конекцијата е веќе отворена и чека во pool-от.
