| Version 6 (modified by , 10 days ago) ( diff ) |
|---|
Други Развојни Активности
Анализа на перформанси
Извештај за диверзификација на портфолио (report_portfolio_diversification)
Анализата на перформанси се врши врз основа на моменталната состојба во базата.
SQL:
CREATE OR REPLACE FUNCTION test_portfolio_diversification()
RETURNS void AS $$
DECLARE
start_time timestamptz;
end_time timestamptz;
duration int;
BEGIN
start_time := clock_timestamp();
PERFORM
ph.portfolio_id,
ph.stock_id,
s.symbol,
ph.quantity::NUMERIC * s.current_price::NUMERIC AS market_value,
ph.quantity::NUMERIC * (s.current_price::NUMERIC
- ph.avg_price::NUMERIC) AS unrealized_pnl
FROM portfolio_holdings ph
JOIN stock s ON ph.stock_id = s.id
WHERE ph.quantity > 0;
end_time := clock_timestamp();
duration := round(1000 * (extract(epoch FROM end_time)
- extract(epoch FROM start_time)));
RAISE NOTICE 'Query executed in: % ms', duration;
END;
$$ LANGUAGE plpgsql;
DROP INDEX IF EXISTS idx_ph_portfolio_id;
DROP INDEX IF EXISTS idx_ph_stock_id;
DROP INDEX IF EXISTS idx_ph_quantity_positive;
DROP INDEX IF EXISTS idx_users_role;
-- run 1: no index
SELECT test_portfolio_diversification();
-- run 2: + idx_ph_stock_id
CREATE INDEX idx_ph_stock_id ON portfolio_holdings(stock_id);
ANALYZE portfolio_holdings;
SELECT test_portfolio_diversification();
-- run 3: + idx_ph_portfolio_id
CREATE INDEX idx_ph_portfolio_id ON portfolio_holdings(portfolio_id);
ANALYZE portfolio_holdings;
SELECT test_portfolio_diversification();
-- run 4: + idx_ph_quantity_positive (partial index)
CREATE INDEX idx_ph_quantity_positive
ON portfolio_holdings(portfolio_id, stock_id)
WHERE quantity > 0;
ANALYZE portfolio_holdings;
SELECT test_portfolio_diversification();
-- run 5: + idx_users_role
CREATE INDEX idx_users_role ON users(role);
ANALYZE users;
SELECT test_portfolio_diversification();
DROP FUNCTION test_portfolio_diversification();
Индекси:
1. portfolio_holdings - Index (portfolio_id)
CREATE INDEX idx_ph_portfolio_id ON portfolio_holdings(portfolio_id);
Користење: GROUP BY и JOIN по portfolio_id во CTE-ата stock_weights и portfolio_totals
Подобрување: Index Scan наместо Seq Scan при групирање по портфолио
---
2. portfolio_holdings - Index (stock_id)
CREATE INDEX idx_ph_stock_id ON portfolio_holdings(stock_id);
Користење: JOIN ph.stock_id = s.id во holding_values CTE
Подобрување: Директен lookup наместо Hash Join со Seq Scan
---
3. portfolio_holdings - Partial Index (quantity > 0)
CREATE INDEX idx_ph_quantity_positive ON portfolio_holdings(portfolio_id, stock_id) WHERE quantity > 0;
Користење: WHERE филтер ph.quantity > 0 во holding_values CTE
Подобрување: Индексот ги содржи само редовите со quantity > 0, елиминирајќи ги нулираните холдинзи уште при скенирањето
---
4. users - Index (role)
CREATE INDEX idx_users_role ON users(role);
Користење: WHERE филтер u.role = 'USER' во финалниот SELECT
Подобрување: Директен Index Scan наместо Seq Scan на целата users табела
---
Сумарно: Без индекси: 201ms
Со индекси: 140ms
Имаме забрзување.
Извештај за волатилност на акции (report_stock_volatility_and_activity)
SQL:
CREATE OR REPLACE FUNCTION test_stock_volatility()
RETURNS void AS $$
DECLARE
start_time timestamptz;
end_time timestamptz;
duration int;
BEGIN
start_time := clock_timestamp();
PERFORM
sh.stock_id,
sh.price,
(sh.price - LAG(sh.price) OVER (
PARTITION BY sh.stock_id ORDER BY sh.timestamp))
/ NULLIF(LAG(sh.price) OVER (
PARTITION BY sh.stock_id ORDER BY sh.timestamp), 0) AS daily_return
FROM stock_history sh
WHERE sh.timestamp >= '2024-01-01'
AND sh.timestamp <= '2024-12-31';
end_time := clock_timestamp();
duration := round(1000 * (extract(epoch FROM end_time)
- extract(epoch FROM start_time)));
RAISE NOTICE 'Query executed in: % ms', duration;
END;
$$ LANGUAGE plpgsql;
DROP INDEX IF EXISTS idx_sh_stock_id_timestamp;
DROP INDEX IF EXISTS idx_tx_stock_id_timestamp;
DROP INDEX IF EXISTS idx_watchlist_stock_id;
DROP INDEX IF EXISTS idx_tr_symbol_status;
-- run 1: no index
SELECT test_stock_volatility();
-- run 2: + idx_sh_stock_id_timestamp (composite)
CREATE INDEX idx_sh_stock_id_timestamp ON stock_history(stock_id, timestamp);
ANALYZE stock_history;
SELECT test_stock_volatility();
-- run 3: + idx_tx_stock_id_timestamp (composite)
CREATE INDEX idx_tx_stock_id_timestamp ON transactions(stock_id, timestamp);
ANALYZE transactions;
SELECT test_stock_volatility();
-- run 4: + idx_watchlist_stock_id
CREATE INDEX idx_watchlist_stock_id ON watchlist(stock_id);
ANALYZE watchlist;
SELECT test_stock_volatility();
-- run 5: + idx_tr_symbol_status (composite)
CREATE INDEX idx_tr_symbol_status ON trade_request(stock_symbol, status);
ANALYZE trade_request;
SELECT test_stock_volatility();
DROP FUNCTION test_stock_volatility();
Индекси:
1. stock_history - Composite Index (stock_id, timestamp)
CREATE INDEX idx_sh_stock_id_timestamp ON stock_history(stock_id, timestamp);
Користење: PARTITION BY sh.stock_id ORDER BY sh.timestamp во LAG прозорската функција и WHERE филтерот за дати
Подобрување: Composite индексот ги покрива и партиционирањето и сортирањето -= елиминира Sort чекор
---
2. transactions - Composite Index (stock_id, timestamp)
CREATE INDEX idx_tx_stock_id_timestamp ON transactions(stock_id, timestamp);
Користење: GROUP BY t.stock_id и WHERE на timestamp во txn_stats
Подобрување: Директен Index Scan наместо Seq Scan + Sort при временски филтрирани агрегации
---
3. watchlist - Index (stock_id)
CREATE INDEX idx_watchlist_stock_id ON watchlist(stock_id);
Користење: GROUP BY stock_id во watchlist_stats
Подобрување: Index Scan при агрегација по акција наместо Seq Scan
---
4. trade_request - Composite Index (stock_symbol, status)
CREATE INDEX idx_tr_symbol_status ON trade_request(stock_symbol, status);
Користење: JOIN tr.stock_symbol = s.symbol и WHERE tr.status = 'PENDING' во pending_stats
Подобрување: Composite индексот ги покрива и JOIN условот и WHERE филтерот истовремено
---
Заклучок:
Без индекси: 102ms
Со индекси: 40ms
Имаме забрзување.
Безбедност и заштита
JWT Token Authorization (Spring Security)
JWT е stateless начин на автентикација - серверот НЕ чува информации за активни сесии во база, туку сите потребни податоци се енкодирани во самиот токен кој корисникот го чува локално. JWT содржи енкодирани информации: user_id, email, role, expiry.
Имплементацијата во SecurityConfig.java дефинира кои endpoints се јавни и кои бараат автентикација, со посебни правила за ADMIN:
// SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// Public endpoints - no token required
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/stocks/**").permitAll()
.requestMatchers("/ws/**", "/topic/**").permitAll()
.requestMatchers("/api/history/**").permitAll()
// Admin only - role-based access control
.requestMatchers("/api/trades/*/approve").hasAuthority("ADMIN")
.requestMatchers("/api/trades/*/decline").hasAuthority("ADMIN")
.requestMatchers("/api/trades/pending").hasAuthority("ADMIN")
// Authenticated users
.requestMatchers("/api/trades/**").authenticated()
.requestMatchers("/api/watchlist/**").authenticated()
.requestMatchers("/api/transactions/**").authenticated()
.anyRequest().authenticated()
)
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Секој HTTP барање поминува низ JwtAuthenticationFilter пред да стигне до контролерот. Филтерот го верификува потписот на токенот, го проверува expiry и го вчитува корисникот. Корисник без валиден JWT токен добива HTTP 401 Unauthorized.
Хеширање на лозинки (BCrypt)
Лозинките на корисниците се чуваат во базата во хеширана форма преку BCrypt алгоритам, а никогаш како plain text.
Во базата колоната users.password содржи BCrypt hash со формат $2a$10$....
CORS Конфигурација
CORS е безбеден механизам кој ги ограничува HTTP барањата само од дозволени домени.
Апликацијата има двојна CORS конфигурација:
// SecurityConfig.java - CORS на Spring Security ниво
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:5173",
"http://localhost:5174",
"http://localhost:5175",
"http://localhost:5176",
"http://localhost:3000"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", source);
return source;
}
// WebConfig.java - CORS на Spring MVC ниво
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:5173",
"http://localhost:5174",
"http://localhost:5175",
"http://localhost:3000"
)
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}
Само барања кои потекнуваат од дозволените localhost портови ќе бидат прифатени. Секое барање од непознат домен добива HTTP 403 уште на ниво на CORS preflight проверката.
Заштита на податоци на ниво на база
На ниво на базата на податоци, безбедноста е обезбедена преку:
- Role-based CHECK constraints -
users.role CHECK (role IN ('USER','ADMIN'))спречува внесување на невалидни улоги директно во базата - Enum CHECK constraints -
transactions.origin CHECK (origin IN ('INTERNAL','EXTERNAL'))иuser_auth_providers.auth_providers CHECK (auth_providers IN ('INTERNAL','GOOGLE'))обезбедуваат интегритет на enum вредностите - Foreign key constraints - сите референцијални правила се дефинирани на ниво на база, не само на апликациско ниво
- UNIQUE constraints -
users.email UNIQUE,users.username UNIQUE,stock.symbol UNIQUEиportfolios.user_id UNIQUEспречуваат дупликати кои би можеле да доведат до безбедносни пропусти
...
