formula project
110
backend/pom.xml
Executable file
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>f1</groupId>
|
||||
<artifactId>f1db</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<java.version>17</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<spring-boot.version>3.0.2</spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<mainClass>f1.Main</mainClass>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>repackage</id>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
46
backend/src/main/java/f1/AppConfig.java
Executable file
@@ -0,0 +1,46 @@
|
||||
package f1;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import f1.db.*;
|
||||
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
@Value("${spring.datasource.url}")
|
||||
private String dbUrl;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String dbUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String dbPassword;
|
||||
|
||||
@Bean
|
||||
public Database database() {
|
||||
Database db = new Database(dbUrl, dbUsername, dbPassword);
|
||||
db.init();
|
||||
return db;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDao userDao(Database db) {
|
||||
return new UserDao(db);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DriverDao driverDao(Database db) {
|
||||
return new DriverDao(db);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TeamDao teamDao(Database db) {
|
||||
return new TeamDao(db);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RaceDao raceDao(Database db) {
|
||||
return new RaceDao(db);
|
||||
}
|
||||
}
|
||||
38
backend/src/main/java/f1/Main.java
Executable file
@@ -0,0 +1,38 @@
|
||||
package f1;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import f1.db.Database;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@SpringBootApplication
|
||||
@Component
|
||||
public class Main {
|
||||
public static String JDBC_URL;
|
||||
public static String JDBC_USER;
|
||||
public static String JDBC_PASSWORD;
|
||||
|
||||
@Value("${spring.datasource.url}")
|
||||
private String dbUrl;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String dbUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String dbPassword;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
JDBC_URL = dbUrl;
|
||||
JDBC_USER = dbUsername;
|
||||
JDBC_PASSWORD = dbPassword;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Main.class, args);
|
||||
var db = new Database(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
|
||||
db.init();
|
||||
}
|
||||
}
|
||||
90
backend/src/main/java/f1/controller/AuthController.java
Executable file
@@ -0,0 +1,90 @@
|
||||
package f1.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import f1.Main;
|
||||
import f1.db.Database;
|
||||
import f1.db.UserDao;
|
||||
import f1.entity.User;
|
||||
import f1.security.JwtUtil;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
|
||||
private Database db;
|
||||
private UserDao userDao;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
public AuthController() {
|
||||
this.db = new Database(Main.JDBC_URL, Main.JDBC_USER, Main.JDBC_PASSWORD);
|
||||
db.init();
|
||||
this.userDao = new UserDao(db);
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> register(@RequestBody User user) {
|
||||
if (userDao.getUserByUsername(user.getUsername()) != null) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body("Username already exists");
|
||||
}
|
||||
|
||||
String encodedPassword = passwordEncoder.encode(user.getPassword());
|
||||
user.setPassword(encodedPassword);
|
||||
|
||||
boolean success = userDao.createUser(user);
|
||||
if (success) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully");
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Registration failed");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody Map<String, String> credentials, HttpServletResponse response) {
|
||||
String username = credentials.get("username");
|
||||
String password = credentials.get("password");
|
||||
|
||||
User user = userDao.getUserByUsername(username);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password");
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(password, user.getPassword())) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password");
|
||||
}
|
||||
|
||||
String token = jwtUtil.generateToken(user.getUsername());
|
||||
|
||||
Cookie cookie = new Cookie("auth_token", token);
|
||||
cookie.setHttpOnly(true);
|
||||
cookie.setSecure(false);
|
||||
cookie.setPath("/"); // 全局有效
|
||||
cookie.setMaxAge(24 * 60 * 60); // 24小时过期
|
||||
|
||||
response.addCookie(cookie);
|
||||
|
||||
return ResponseEntity.ok("Login successful");
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<?> logout(HttpServletResponse response) {
|
||||
Cookie cookie = new Cookie("auth_token", null);
|
||||
cookie.setHttpOnly(true);
|
||||
cookie.setPath("/");
|
||||
cookie.setMaxAge(0); // 立即过期
|
||||
response.addCookie(cookie);
|
||||
return ResponseEntity.ok("Logged out");
|
||||
}
|
||||
}
|
||||
82
backend/src/main/java/f1/controller/CommentController.java
Executable file
@@ -0,0 +1,82 @@
|
||||
package f1.controller;
|
||||
|
||||
import f1.db.UserDao;
|
||||
import f1.entity.CommentItem;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.security.Principal;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/comments")
|
||||
public class CommentController {
|
||||
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
|
||||
@GetMapping
|
||||
public List<List<CommentItem>> getComments(@RequestParam(defaultValue = "10") int limit,
|
||||
@RequestParam(defaultValue = "0") int offset,
|
||||
@RequestParam(required = false) Integer pageSize) {
|
||||
List<CommentItem> allComments = userDao.getRootComments(limit, offset);
|
||||
List<List<CommentItem>> pages = new ArrayList<>();
|
||||
|
||||
if (allComments.isEmpty()) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
int size = (pageSize == null || pageSize <= 0) ? allComments.size() : pageSize;
|
||||
|
||||
for (int i = 0; i < allComments.size(); i += size) {
|
||||
int end = Math.min(allComments.size(), i + size);
|
||||
pages.add(allComments.subList(i, end));
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public boolean addRootComment(@RequestBody Map<String, Object> payload) {
|
||||
int userId = (int) payload.get("user_id");
|
||||
String content = (String) payload.get("content");
|
||||
return userDao.addComment(userId, null, null, content);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/replies")
|
||||
public Map<String, Object> getChildComments(@PathVariable int id) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("parent", userDao.getCommentById(id));
|
||||
response.put("replies", userDao.getChildComments(id));
|
||||
return response;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/replies")
|
||||
public boolean addChildComment(@PathVariable int id, @RequestBody Map<String, Object> payload) {
|
||||
int userId = (int) payload.get("user_id");
|
||||
String content = (String) payload.get("content");
|
||||
Integer responseId = payload.containsKey("response_id") ? (Integer) payload.get("response_id") : null;
|
||||
|
||||
if (responseId == null) {
|
||||
responseId = id;
|
||||
}
|
||||
|
||||
return userDao.addComment(userId, responseId, id, content);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public boolean deleteComment(@PathVariable int id, Principal principal) {
|
||||
CommentItem comment = userDao.getCommentById(id);
|
||||
if (comment == null) {
|
||||
return false;
|
||||
}
|
||||
if (!comment.getUsername().equals(principal.getName())) {
|
||||
return false;
|
||||
}
|
||||
return userDao.deleteComment(id);
|
||||
}
|
||||
}
|
||||
115
backend/src/main/java/f1/controller/F1Controller.java
Executable file
@@ -0,0 +1,115 @@
|
||||
package f1.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import f1.db.DriverDao;
|
||||
import f1.db.TeamDao;
|
||||
import f1.entity.Driver;
|
||||
import f1.entity.DriverHistoryItem;
|
||||
import f1.entity.DriverStatistic;
|
||||
import f1.entity.QualifyingResultItem;
|
||||
import f1.entity.RaceResultItem;
|
||||
import f1.entity.SeasonDriver;
|
||||
import f1.entity.SeasonScheduleItem;
|
||||
import f1.entity.Team;
|
||||
import f1.entity.TeamHistoryItem;
|
||||
import f1.entity.TeamStatistic;
|
||||
import f1.db.RaceDao;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class F1Controller {
|
||||
|
||||
@Autowired
|
||||
private DriverDao driverDao;
|
||||
@Autowired
|
||||
private TeamDao teamDao;
|
||||
@Autowired
|
||||
private RaceDao raceDao;
|
||||
|
||||
@GetMapping("/drivers")
|
||||
public List<Driver> getAllDrivers() {
|
||||
return driverDao.getAllDrivers();
|
||||
}
|
||||
|
||||
@GetMapping("/teams")
|
||||
public List<Team> getAllTeams() {
|
||||
return teamDao.getAllTeams();
|
||||
}
|
||||
|
||||
@GetMapping("/season-drivers")
|
||||
public List<SeasonDriver> getDriversBySeason(@RequestParam int season) {
|
||||
return driverDao.getDriversBySeason(season);
|
||||
}
|
||||
|
||||
@GetMapping("/standings/teams")
|
||||
public List<Map<String, Object>> getSeasonTeamStandings(@RequestParam int season) {
|
||||
return teamDao.getSeasonTop5TeamStandings(season);
|
||||
}
|
||||
|
||||
@GetMapping("/standings/drivers")
|
||||
public List<Map<String, Object>> getSeasonDriverStandings(@RequestParam int season) {
|
||||
return driverDao.getSeasonTop5DriverStandings(season);
|
||||
}
|
||||
|
||||
@GetMapping("/prix")
|
||||
public List<SeasonScheduleItem> getSeasonSchedule(@RequestParam int season) {
|
||||
return raceDao.getSeasonSchedule(season);
|
||||
}
|
||||
|
||||
@GetMapping("/teams/{id}/drivers")
|
||||
public List<String> getDriverNames(@PathVariable int id, @RequestParam int season) {
|
||||
return driverDao.getDriverNamesByTeamAndSeason(id, season);
|
||||
}
|
||||
|
||||
@GetMapping("/drivers/{id}/statistics")
|
||||
public Map<String, DriverStatistic> getDriverStatistic(@PathVariable int id, @RequestParam int season) {
|
||||
Map<String, DriverStatistic> result = new HashMap<>();
|
||||
result.put("formal", driverDao.getDriverStatistic(id, season, false));
|
||||
result.put("sprint", driverDao.getDriverStatistic(id, season, true));
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/teams/{id}/statistics")
|
||||
public TeamStatistic getTeamStatistic(@PathVariable int id, @RequestParam int season) {
|
||||
DriverStatistic formal = teamDao.getTeamStatistic(id, season, false);
|
||||
DriverStatistic sprint = teamDao.getTeamStatistic(id, season, true);
|
||||
List<String> drivers = driverDao.getDriverNamesByTeamAndSeason(id, season);
|
||||
|
||||
Map<String, Integer> standing = teamDao.getTeamStanding(id, season);
|
||||
int rank = standing.getOrDefault("ranking", 0);
|
||||
int totalScore = standing.getOrDefault("score", 0);
|
||||
|
||||
return new TeamStatistic(formal, sprint, drivers, rank, totalScore);
|
||||
}
|
||||
|
||||
@GetMapping("/prix/{prixId}/race")
|
||||
public List<RaceResultItem> getRaceResult(@PathVariable int prixId,
|
||||
@RequestParam(defaultValue = "false") boolean isSprint) {
|
||||
return raceDao.getRaceResults(prixId, isSprint);
|
||||
}
|
||||
|
||||
@GetMapping("/prix/{prixId}/qualifying")
|
||||
public List<QualifyingResultItem> getQualifyingResult(@PathVariable int prixId,
|
||||
@RequestParam(defaultValue = "false") boolean isSprint) {
|
||||
return raceDao.getQualifyingResults(prixId, isSprint);
|
||||
}
|
||||
|
||||
@GetMapping("/drivers/{id}/results")
|
||||
public List<DriverHistoryItem> getDriverHistory(@PathVariable int id, @RequestParam int season) {
|
||||
return driverDao.getDriverHistory(id, season);
|
||||
}
|
||||
|
||||
@GetMapping("/teams/{id}/results")
|
||||
public List<TeamHistoryItem> getTeamHistory(@PathVariable int id, @RequestParam int season) {
|
||||
return teamDao.getTeamHistory(id, season);
|
||||
}
|
||||
}
|
||||
35
backend/src/main/java/f1/controller/UserController.java
Executable file
@@ -0,0 +1,35 @@
|
||||
package f1.controller;
|
||||
|
||||
import f1.db.UserDao;
|
||||
import f1.entity.User;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserDao userDao;
|
||||
|
||||
@GetMapping
|
||||
public User getCurrentUser(Principal principal) {
|
||||
if (principal == null) {
|
||||
return null; // Or throw exception / return 401
|
||||
}
|
||||
String username = principal.getName();
|
||||
// getUserByUsername returns full user, but password is now @JsonIgnore-d
|
||||
return userDao.getUserByUsername(username);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public User getUserById(@PathVariable int id) {
|
||||
return userDao.getUserById(id);
|
||||
}
|
||||
}
|
||||
16
backend/src/main/java/f1/db/BaseDao.java
Executable file
@@ -0,0 +1,16 @@
|
||||
package f1.db;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public abstract class BaseDao {
|
||||
protected Database db;
|
||||
|
||||
public BaseDao(Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
protected Connection getConnection() throws SQLException {
|
||||
return db.getConnection();
|
||||
}
|
||||
}
|
||||
66
backend/src/main/java/f1/db/Database.java
Executable file
@@ -0,0 +1,66 @@
|
||||
package f1.db;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Database {
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
public Database(String url, String user, String password) {
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(url);
|
||||
config.setUsername(user);
|
||||
config.setPassword(password);
|
||||
config.addDataSourceProperty("cachePrepStmts", "true");
|
||||
config.addDataSourceProperty("prepStmtCacheSize", "250");
|
||||
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
||||
config.setMaximumPoolSize(10);
|
||||
|
||||
this.dataSource = new HikariDataSource(config);
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
try (Connection con = getConnection()) {
|
||||
executeSqlFile(con, "/init.sql");
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void executeSqlFile(Connection con, String resourcePath) {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(getClass().getResourceAsStream(resourcePath)));
|
||||
var stmt = con.createStatement()) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.startsWith("--") || line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
sql.append(line).append(" ");
|
||||
if (line.endsWith(";")) {
|
||||
stmt.executeUpdate(sql.toString().trim());
|
||||
sql.setLength(0);
|
||||
}
|
||||
}
|
||||
} catch (IOException | SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
191
backend/src/main/java/f1/db/DriverDao.java
Executable file
@@ -0,0 +1,191 @@
|
||||
package f1.db;
|
||||
|
||||
import f1.entity.Driver;
|
||||
import f1.entity.DriverHistoryItem;
|
||||
import f1.entity.DriverStatistic;
|
||||
import f1.entity.SeasonDriver;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DriverDao extends BaseDao {
|
||||
|
||||
public DriverDao(Database db) {
|
||||
super(db);
|
||||
}
|
||||
|
||||
public ArrayList<Driver> getAllDrivers() {
|
||||
ArrayList<Driver> drivers = new ArrayList<>();
|
||||
try (var con = getConnection();
|
||||
var stmt = con.createStatement();
|
||||
var rs = stmt.executeQuery("SELECT * FROM driver")) {
|
||||
while (rs.next()) {
|
||||
int id = rs.getInt("id");
|
||||
String name = rs.getString("name");
|
||||
String country = rs.getString("country");
|
||||
java.sql.Date birthday = rs.getDate("birthday");
|
||||
drivers.add(new Driver(id, name, country, birthday));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return drivers;
|
||||
}
|
||||
|
||||
public ArrayList<SeasonDriver> getDriversBySeason(int season) {
|
||||
ArrayList<SeasonDriver> seasonDrivers = new ArrayList<>();
|
||||
try (var con = getConnection();
|
||||
var stmt = con
|
||||
.prepareStatement("SELECT * FROM season_driver WHERE season = ? ORDER BY team, car_num")) {
|
||||
stmt.setInt(1, season);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
int id = rs.getInt("id");
|
||||
String name = rs.getString("name");
|
||||
String team = rs.getString("team");
|
||||
String country = rs.getString("country");
|
||||
java.sql.Date birthday = rs.getDate("birthday");
|
||||
int carNum = rs.getInt("car_num");
|
||||
int seasonVal = rs.getInt("season");
|
||||
seasonDrivers.add(new SeasonDriver(id, name, team, country, birthday, carNum, seasonVal));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return seasonDrivers;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getSeasonTop5DriverStandings(int season) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
String sql = "SELECT sd.name, sd.team, standings.total_score, standings.ranking " +
|
||||
"FROM season_driver sd " +
|
||||
"JOIN ( " +
|
||||
" SELECT rr.driver_id, SUM(rr.score) AS total_score, " +
|
||||
" RANK() OVER (ORDER BY SUM(rr.score) DESC) AS ranking " +
|
||||
" FROM race_result rr " +
|
||||
" JOIN prix p ON rr.prix_id = p.id " +
|
||||
" WHERE p.season = ? " +
|
||||
" GROUP BY rr.driver_id " +
|
||||
") standings ON sd.id = standings.driver_id " +
|
||||
"WHERE sd.season = ? " +
|
||||
"ORDER BY standings.ranking ASC " +
|
||||
"LIMIT 5";
|
||||
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, season);
|
||||
stmt.setInt(2, season);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("driver_name", rs.getString("name"));
|
||||
item.put("team_name", rs.getString("team"));
|
||||
item.put("total_score", rs.getInt("total_score"));
|
||||
item.put("ranking", rs.getInt("ranking"));
|
||||
result.add(item);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public ArrayList<String> getDriverNamesByTeamAndSeason(int teamId, int season) {
|
||||
ArrayList<String> names = new ArrayList<>();
|
||||
String sql = "SELECT d.name FROM driver d JOIN contract c ON d.id = c.driver_id WHERE c.season = ? AND c.team_id = ?";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, season);
|
||||
stmt.setInt(2, teamId);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
names.add(rs.getString("name"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public DriverStatistic getDriverStatistic(int driverId, int season, boolean isSprint) {
|
||||
return getStatistic("rr.driver_id = ?", driverId, season, isSprint);
|
||||
}
|
||||
|
||||
private DriverStatistic getStatistic(String whereClause, int id, int season, boolean isSprint) {
|
||||
DriverStatistic statistic = null;
|
||||
String sql = """
|
||||
SELECT
|
||||
COUNT(DISTINCT prix_id) AS total_cnt,
|
||||
SUM(score) AS score_sum,
|
||||
SUM(end_position <= 3) AS medal,
|
||||
SUM(end_position = 1) AS gold,
|
||||
SUM(start_position = 1) AS pole,
|
||||
SUM(end_position <= 10) AS top_ten,
|
||||
SUM(finish_time IN ('DNF', 'DNS', 'DSQ')) AS unfinished,
|
||||
SUM(fastest_time = fastest.fastest) AS fastest_lap
|
||||
FROM
|
||||
race_result rr
|
||||
NATURAL JOIN (
|
||||
SELECT
|
||||
rr.prix_id ,
|
||||
MIN(rr.fastest_time) AS fastest
|
||||
FROM
|
||||
race_result rr
|
||||
WHERE
|
||||
rr.fastest_time NOT IN ('DNF', 'DNS', 'DSQ')
|
||||
AND rr.is_sprint = ?
|
||||
AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?)
|
||||
GROUP BY
|
||||
rr.prix_id) fastest
|
||||
WHERE
|
||||
%s
|
||||
AND rr.is_sprint = ?
|
||||
AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?)
|
||||
""".formatted(whereClause);
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setBoolean(1, isSprint);
|
||||
stmt.setInt(2, season);
|
||||
stmt.setInt(3, id);
|
||||
stmt.setBoolean(4, isSprint);
|
||||
stmt.setInt(5, season);
|
||||
var rs = stmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
int totalCnt = rs.getInt("total_cnt");
|
||||
int scoreSum = rs.getInt("score_sum");
|
||||
int medal = rs.getInt("medal");
|
||||
int gold = rs.getInt("gold");
|
||||
int pole = rs.getInt("pole");
|
||||
int topTen = rs.getInt("top_ten");
|
||||
int unfinished = rs.getInt("unfinished");
|
||||
int fastestLap = rs.getInt("fastest_lap");
|
||||
statistic = new DriverStatistic(totalCnt, scoreSum, medal, gold, pole, topTen, unfinished,
|
||||
fastestLap);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return statistic;
|
||||
}
|
||||
|
||||
public ArrayList<DriverHistoryItem> getDriverHistory(int driverId, int season) {
|
||||
ArrayList<DriverHistoryItem> history = new ArrayList<>();
|
||||
String sql = "SELECT prix_name, team_name, pos, score, is_sprint FROM prix_result WHERE driver_id = ? AND season = ? ORDER BY round, is_sprint";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, driverId);
|
||||
stmt.setInt(2, season);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
history.add(new DriverHistoryItem(
|
||||
rs.getString("prix_name"),
|
||||
rs.getString("team_name"),
|
||||
rs.getInt("pos"),
|
||||
rs.getInt("score"),
|
||||
rs.getBoolean("is_sprint")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return history;
|
||||
}
|
||||
}
|
||||
120
backend/src/main/java/f1/db/RaceDao.java
Executable file
@@ -0,0 +1,120 @@
|
||||
package f1.db;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import f1.entity.QualifyingResultItem;
|
||||
import f1.entity.RaceResultItem;
|
||||
import f1.entity.SeasonScheduleItem;
|
||||
|
||||
public class RaceDao extends BaseDao {
|
||||
|
||||
public RaceDao(Database db) {
|
||||
super(db);
|
||||
}
|
||||
|
||||
public ArrayList<RaceResultItem> getRaceResults(int prixId, boolean isSprint) {
|
||||
ArrayList<RaceResultItem> results = new ArrayList<>();
|
||||
String sql = "SELECT pos, car_num, driver_name, team_name, finish_time, score FROM prix_result WHERE prix_id = ? AND is_sprint = ? ORDER BY pos";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, prixId);
|
||||
stmt.setBoolean(2, isSprint);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
results.add(new RaceResultItem(
|
||||
rs.getInt("pos"),
|
||||
rs.getInt("car_num"),
|
||||
rs.getString("driver_name"),
|
||||
rs.getString("team_name"),
|
||||
rs.getString("finish_time"),
|
||||
rs.getInt("score")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public ArrayList<QualifyingResultItem> getQualifyingResults(int prixId, boolean isSprint) {
|
||||
ArrayList<QualifyingResultItem> results = new ArrayList<>();
|
||||
String sql = """
|
||||
SELECT
|
||||
qr.position,
|
||||
sd.car_num,
|
||||
sd.name,
|
||||
sd.team,
|
||||
MAX(CASE WHEN section = 1 THEN fastest_time END) AS q1_time,
|
||||
MAX(CASE WHEN section = 2 THEN fastest_time END) AS q2_time,
|
||||
MAX(CASE WHEN section = 3 THEN fastest_time END) AS q3_time
|
||||
FROM
|
||||
qualifying_result qr
|
||||
JOIN
|
||||
prix p ON qr.prix_id = p.id
|
||||
JOIN
|
||||
season_driver sd ON qr.driver_id = sd.id AND sd.season = p.season
|
||||
WHERE
|
||||
qr.prix_id = ?
|
||||
AND qr.is_sprint = ?
|
||||
GROUP BY
|
||||
qr.driver_id, qr.position, sd.car_num, sd.name, sd.team
|
||||
ORDER BY
|
||||
qr.position
|
||||
""";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, prixId);
|
||||
stmt.setBoolean(2, isSprint);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
results.add(new QualifyingResultItem(
|
||||
rs.getInt("position"),
|
||||
rs.getInt("car_num"),
|
||||
rs.getString("name"),
|
||||
rs.getString("team"),
|
||||
rs.getString("q1_time"),
|
||||
rs.getString("q2_time"),
|
||||
rs.getString("q3_time")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public ArrayList<SeasonScheduleItem> getSeasonSchedule(int season) {
|
||||
ArrayList<SeasonScheduleItem> schedule = new ArrayList<>();
|
||||
String sql = """
|
||||
SELECT
|
||||
p.id,
|
||||
p.round,
|
||||
p.name,
|
||||
p.have_sprint,
|
||||
p.circuit_id,
|
||||
c.country,
|
||||
c.location
|
||||
FROM
|
||||
prix p
|
||||
JOIN circuit c ON c.id = p.circuit_id
|
||||
WHERE
|
||||
p.season = ?
|
||||
ORDER BY
|
||||
p.round
|
||||
""";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, season);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
schedule.add(new SeasonScheduleItem(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("round"),
|
||||
rs.getString("name"),
|
||||
rs.getBoolean("have_sprint"),
|
||||
rs.getInt("circuit_id"),
|
||||
rs.getString("country"),
|
||||
rs.getString("location")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return schedule;
|
||||
}
|
||||
}
|
||||
158
backend/src/main/java/f1/db/TeamDao.java
Executable file
@@ -0,0 +1,158 @@
|
||||
package f1.db;
|
||||
|
||||
import f1.entity.DriverStatistic;
|
||||
import f1.entity.Team;
|
||||
import f1.entity.TeamHistoryItem;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class TeamDao extends BaseDao {
|
||||
|
||||
public TeamDao(Database db) {
|
||||
super(db);
|
||||
}
|
||||
|
||||
public ArrayList<Team> getAllTeams() {
|
||||
ArrayList<Team> teams = new ArrayList<>();
|
||||
try (var con = getConnection();
|
||||
var stmt = con.createStatement();
|
||||
var rs = stmt.executeQuery("SELECT * FROM team")) {
|
||||
while (rs.next()) {
|
||||
int id = rs.getInt("id");
|
||||
String name = rs.getString("name");
|
||||
String country = rs.getString("country");
|
||||
String engineSupplier = rs.getString("engine_supplier");
|
||||
int setupYear = rs.getInt("setup_time");
|
||||
java.sql.Date setupTime = java.sql.Date.valueOf(LocalDate.of(setupYear, 1, 1));
|
||||
teams.add(new Team(id, name, country, engineSupplier, setupTime));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return teams;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getTeamStanding(int teamId, int season) {
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
String sql = "SELECT total_score, ranking FROM season_team_standings WHERE season = ? AND team_id = ?";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, season);
|
||||
stmt.setInt(2, teamId);
|
||||
var rs = stmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
result.put("score", rs.getInt("total_score"));
|
||||
result.put("ranking", rs.getInt("ranking"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getSeasonTop5TeamStandings(int season) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
String sql = "SELECT t.name, s.total_score, s.ranking " +
|
||||
"FROM season_team_standings s " +
|
||||
"JOIN team t ON s.team_id = t.id " +
|
||||
"WHERE s.season = ? " +
|
||||
"ORDER BY s.ranking ASC " +
|
||||
"LIMIT 5";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, season);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("team_name", rs.getString("name"));
|
||||
item.put("total_score", rs.getInt("total_score"));
|
||||
item.put("ranking", rs.getInt("ranking"));
|
||||
result.add(item);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public DriverStatistic getTeamStatistic(int teamId, int season, boolean isSprint) {
|
||||
return getStatistic("rr.team_id = ?", teamId, season, isSprint);
|
||||
}
|
||||
|
||||
private DriverStatistic getStatistic(String whereClause, int id, int season, boolean isSprint) {
|
||||
DriverStatistic statistic = null;
|
||||
String sql = """
|
||||
SELECT
|
||||
COUNT(DISTINCT prix_id) AS total_cnt,
|
||||
SUM(score) AS score_sum,
|
||||
SUM(end_position <= 3) AS medal,
|
||||
SUM(end_position = 1) AS gold,
|
||||
SUM(start_position = 1) AS pole,
|
||||
SUM(end_position <= 10) AS top_ten,
|
||||
SUM(finish_time IN ('DNF', 'DNS', 'DSQ')) AS unfinished,
|
||||
SUM(fastest_time = fastest.fastest) AS fastest_lap
|
||||
FROM
|
||||
race_result rr
|
||||
NATURAL JOIN (
|
||||
SELECT
|
||||
rr.prix_id ,
|
||||
MIN(rr.fastest_time) AS fastest
|
||||
FROM
|
||||
race_result rr
|
||||
WHERE
|
||||
rr.fastest_time NOT IN ('DNF', 'DNS', 'DSQ')
|
||||
AND rr.is_sprint = ?
|
||||
AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?)
|
||||
GROUP BY
|
||||
rr.prix_id) fastest
|
||||
WHERE
|
||||
%s
|
||||
AND rr.is_sprint = ?
|
||||
AND rr.prix_id IN (SELECT id FROM prix WHERE season = ?)
|
||||
""".formatted(whereClause);
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setBoolean(1, isSprint);
|
||||
stmt.setInt(2, season);
|
||||
stmt.setInt(3, id);
|
||||
stmt.setBoolean(4, isSprint);
|
||||
stmt.setInt(5, season);
|
||||
var rs = stmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
int totalCnt = rs.getInt("total_cnt");
|
||||
int scoreSum = rs.getInt("score_sum");
|
||||
int medal = rs.getInt("medal");
|
||||
int gold = rs.getInt("gold");
|
||||
int pole = rs.getInt("pole");
|
||||
int topTen = rs.getInt("top_ten");
|
||||
int unfinished = rs.getInt("unfinished");
|
||||
int fastestLap = rs.getInt("fastest_lap");
|
||||
statistic = new DriverStatistic(totalCnt, scoreSum, medal, gold, pole, topTen, unfinished,
|
||||
fastestLap);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return statistic;
|
||||
}
|
||||
|
||||
public ArrayList<TeamHistoryItem> getTeamHistory(int teamId, int season) {
|
||||
ArrayList<TeamHistoryItem> history = new ArrayList<>();
|
||||
String sql = "SELECT prix_name, SUM(score) as total_score FROM prix_result WHERE team_id = ? AND season = ? GROUP BY prix_id, prix_name, round ORDER BY round";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, teamId);
|
||||
stmt.setInt(2, season);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
history.add(new TeamHistoryItem(
|
||||
rs.getString("prix_name"),
|
||||
rs.getInt("total_score")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return history;
|
||||
}
|
||||
}
|
||||
200
backend/src/main/java/f1/db/UserDao.java
Executable file
@@ -0,0 +1,200 @@
|
||||
package f1.db;
|
||||
|
||||
import f1.entity.CommentItem;
|
||||
import f1.entity.User;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class UserDao extends BaseDao {
|
||||
|
||||
public UserDao(Database db) {
|
||||
super(db);
|
||||
}
|
||||
|
||||
public boolean createUser(User user) {
|
||||
String sql = "INSERT INTO user (username, password, email, country, avatar) VALUES (?, ?, ?, ?, ?)";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setString(1, user.getUsername());
|
||||
stmt.setString(2, user.getPassword());
|
||||
stmt.setString(3, user.getEmail());
|
||||
stmt.setString(4, user.getCountry());
|
||||
stmt.setString(5, user.getAvatar());
|
||||
int affectedRows = stmt.executeUpdate();
|
||||
return affectedRows > 0;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public User getUserByUsername(String username) {
|
||||
String sql = "SELECT * FROM user WHERE username = ?";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
var rs = stmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
return new User(
|
||||
rs.getInt("id"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password"),
|
||||
rs.getString("email"),
|
||||
rs.getString("country"),
|
||||
rs.getString("avatar"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public User getUserById(int id) {
|
||||
String sql = "SELECT username, email, country, avatar FROM user WHERE id = ?";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, id);
|
||||
var rs = stmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
// Password is not selected, so we pass null
|
||||
return new User(
|
||||
id,
|
||||
rs.getString("username"),
|
||||
null, // password
|
||||
rs.getString("email"),
|
||||
rs.getString("country"),
|
||||
rs.getString("avatar"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean addComment(int userId, Integer responseId, Integer rootId, String content) {
|
||||
String sql = "INSERT INTO comment (user_id, response_id, root_id, content) VALUES (?, ?, ?, ?)";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, userId);
|
||||
|
||||
if (responseId == null) {
|
||||
stmt.setNull(2, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
stmt.setInt(2, responseId);
|
||||
}
|
||||
|
||||
if (rootId == null) {
|
||||
stmt.setNull(3, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
stmt.setInt(3, rootId);
|
||||
}
|
||||
|
||||
stmt.setString(4, content);
|
||||
int affectedRows = stmt.executeUpdate();
|
||||
return affectedRows > 0;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deleteComment(int commentId) {
|
||||
String sql = "DELETE FROM comment WHERE id = ?";
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, commentId);
|
||||
int affectedRows = stmt.executeUpdate();
|
||||
return affectedRows > 0;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<CommentItem> getRootComments(int limit, int offset) {
|
||||
List<CommentItem> comments = new ArrayList<>();
|
||||
String sql = "SELECT c.id, c.user_id, u.username, c.content, COUNT(replies.id) AS reply_count " +
|
||||
"FROM comment c " +
|
||||
"JOIN user u ON c.user_id = u.id " +
|
||||
"LEFT JOIN comment replies ON replies.root_id = c.id " +
|
||||
"WHERE c.root_id IS NULL " +
|
||||
"GROUP BY c.id " +
|
||||
"ORDER BY c.id DESC " +
|
||||
"LIMIT ? OFFSET ?";
|
||||
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, limit);
|
||||
stmt.setInt(2, offset);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
comments.add(new CommentItem(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("user_id"),
|
||||
rs.getString("username"),
|
||||
null, // response_id is null for root
|
||||
null, // root_id is null for root
|
||||
rs.getString("content"),
|
||||
rs.getInt("reply_count")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
|
||||
public List<CommentItem> getChildComments(int rootId) {
|
||||
List<CommentItem> comments = new ArrayList<>();
|
||||
String sql = "SELECT c.id, c.user_id, u.username, c.response_id, c.content, " +
|
||||
"parent_c.user_id AS reply_to_user_id, parent_u.username AS reply_to_username " +
|
||||
"FROM comment c " +
|
||||
"JOIN user u ON c.user_id = u.id " +
|
||||
"LEFT JOIN comment parent_c ON c.response_id = parent_c.id " +
|
||||
"LEFT JOIN user parent_u ON parent_c.user_id = parent_u.id " +
|
||||
"WHERE c.root_id = ? " +
|
||||
"ORDER BY c.id ASC";
|
||||
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, rootId);
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
comments.add(new CommentItem(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("user_id"),
|
||||
rs.getString("username"),
|
||||
(Integer) rs.getObject("response_id"),
|
||||
null, // root_id is known and not needed in response
|
||||
rs.getString("content"),
|
||||
(Integer) rs.getObject("reply_to_user_id"),
|
||||
rs.getString("reply_to_username")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
|
||||
public CommentItem getCommentById(int id) {
|
||||
String sql = "SELECT c.id, c.user_id, u.username, c.content, COUNT(replies.id) AS reply_count " +
|
||||
"FROM comment c " +
|
||||
"JOIN user u ON c.user_id = u.id " +
|
||||
"LEFT JOIN comment replies ON replies.root_id = c.id " +
|
||||
"WHERE c.id = ? " +
|
||||
"GROUP BY c.id";
|
||||
|
||||
try (var con = getConnection(); var stmt = con.prepareStatement(sql)) {
|
||||
stmt.setInt(1, id);
|
||||
var rs = stmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
return new CommentItem(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("user_id"),
|
||||
rs.getString("username"),
|
||||
null,
|
||||
null,
|
||||
rs.getString("content"),
|
||||
rs.getInt("reply_count")
|
||||
);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
90
backend/src/main/java/f1/entity/CommentItem.java
Executable file
@@ -0,0 +1,90 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CommentItem {
|
||||
private int id;
|
||||
|
||||
@JsonProperty("user_id")
|
||||
private int userId;
|
||||
|
||||
private String username;
|
||||
|
||||
@JsonProperty("response_id")
|
||||
private Integer responseId;
|
||||
|
||||
@JsonProperty("root_id")
|
||||
private Integer rootId;
|
||||
|
||||
private String content;
|
||||
|
||||
@JsonProperty("reply_count")
|
||||
private int replyCount;
|
||||
|
||||
@JsonProperty("reply_to_user_id")
|
||||
private Integer replyToUserId;
|
||||
|
||||
@JsonProperty("reply_to_username")
|
||||
private String replyToUsername;
|
||||
|
||||
public CommentItem(int id, int userId, String username, Integer responseId, Integer rootId, String content,
|
||||
int replyCount) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.responseId = responseId;
|
||||
this.rootId = rootId;
|
||||
this.content = content;
|
||||
this.replyCount = replyCount;
|
||||
}
|
||||
|
||||
public CommentItem(int id, int userId, String username, Integer responseId, Integer rootId, String content,
|
||||
Integer replyToUserId, String replyToUsername) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.responseId = responseId;
|
||||
this.rootId = rootId;
|
||||
this.content = content;
|
||||
this.replyToUserId = replyToUserId;
|
||||
this.replyToUsername = replyToUsername;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public Integer getResponseId() {
|
||||
return responseId;
|
||||
}
|
||||
|
||||
public Integer getRootId() {
|
||||
return rootId;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public int getReplyCount() {
|
||||
return replyCount;
|
||||
}
|
||||
|
||||
public Integer getReplyToUserId() {
|
||||
return replyToUserId;
|
||||
}
|
||||
|
||||
public String getReplyToUsername() {
|
||||
return replyToUsername;
|
||||
}
|
||||
}
|
||||
49
backend/src/main/java/f1/entity/Driver.java
Executable file
@@ -0,0 +1,49 @@
|
||||
package f1.entity;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
public class Driver {
|
||||
private int id;
|
||||
private String name;
|
||||
private String country;
|
||||
private Date birthday;
|
||||
|
||||
public Driver(int id, String name, String country, Date birthday) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.country = country;
|
||||
this.birthday = birthday;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public Date getBirthday() {
|
||||
return birthday;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public void setBirthday(Date birthday) {
|
||||
this.birthday = birthday;
|
||||
}
|
||||
}
|
||||
45
backend/src/main/java/f1/entity/DriverHistoryItem.java
Executable file
@@ -0,0 +1,45 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class DriverHistoryItem {
|
||||
@JsonProperty("prix_name")
|
||||
private String prixName;
|
||||
|
||||
@JsonProperty("team_name")
|
||||
private String teamName;
|
||||
|
||||
private int position;
|
||||
private int score;
|
||||
|
||||
@JsonProperty("is_sprint")
|
||||
private boolean isSprint;
|
||||
|
||||
public DriverHistoryItem(String prixName, String teamName, int position, int score, boolean isSprint) {
|
||||
this.prixName = prixName;
|
||||
this.teamName = teamName;
|
||||
this.position = position;
|
||||
this.score = score;
|
||||
this.isSprint = isSprint;
|
||||
}
|
||||
|
||||
public String getPrixName() {
|
||||
return prixName;
|
||||
}
|
||||
|
||||
public String getTeamName() {
|
||||
return teamName;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public boolean isSprint() {
|
||||
return isSprint;
|
||||
}
|
||||
}
|
||||
88
backend/src/main/java/f1/entity/DriverStatistic.java
Executable file
@@ -0,0 +1,88 @@
|
||||
package f1.entity;
|
||||
|
||||
public class DriverStatistic {
|
||||
private int totalCnt;
|
||||
private int scoreSum;
|
||||
private int medal;
|
||||
private int gold;
|
||||
private int pole;
|
||||
private int topTen;
|
||||
private int unfinished;
|
||||
private int fastestLap;
|
||||
|
||||
public DriverStatistic(int totalCnt, int scoreSum, int medal, int gold, int pole, int topTen, int unfinished,
|
||||
int fastestLap) {
|
||||
this.totalCnt = totalCnt;
|
||||
this.scoreSum = scoreSum;
|
||||
this.medal = medal;
|
||||
this.gold = gold;
|
||||
this.pole = pole;
|
||||
this.topTen = topTen;
|
||||
this.unfinished = unfinished;
|
||||
this.fastestLap = fastestLap;
|
||||
}
|
||||
|
||||
public int getTotalCnt() {
|
||||
return totalCnt;
|
||||
}
|
||||
|
||||
public int getScoreSum() {
|
||||
return scoreSum;
|
||||
}
|
||||
|
||||
public int getMedal() {
|
||||
return medal;
|
||||
}
|
||||
|
||||
public int getGold() {
|
||||
return gold;
|
||||
}
|
||||
|
||||
public int getPole() {
|
||||
return pole;
|
||||
}
|
||||
|
||||
public int getTopTen() {
|
||||
return topTen;
|
||||
}
|
||||
|
||||
public int getUnfinished() {
|
||||
return unfinished;
|
||||
}
|
||||
|
||||
public int getFastestLap() {
|
||||
return fastestLap;
|
||||
}
|
||||
|
||||
public void setTotalCnt(int totalCnt) {
|
||||
this.totalCnt = totalCnt;
|
||||
}
|
||||
|
||||
public void setScoreSum(int scoreSum) {
|
||||
this.scoreSum = scoreSum;
|
||||
}
|
||||
|
||||
public void setMedal(int medal) {
|
||||
this.medal = medal;
|
||||
}
|
||||
|
||||
public void setGold(int gold) {
|
||||
this.gold = gold;
|
||||
}
|
||||
|
||||
public void setPole(int pole) {
|
||||
this.pole = pole;
|
||||
}
|
||||
|
||||
public void setTopTen(int topTen) {
|
||||
this.topTen = topTen;
|
||||
}
|
||||
|
||||
public void setUnfinished(int unfinished) {
|
||||
this.unfinished = unfinished;
|
||||
}
|
||||
|
||||
public void setFastestLap(int fastestLap) {
|
||||
this.fastestLap = fastestLap;
|
||||
}
|
||||
}
|
||||
89
backend/src/main/java/f1/entity/QualifyingResultItem.java
Executable file
@@ -0,0 +1,89 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class QualifyingResultItem {
|
||||
private int position;
|
||||
|
||||
@JsonProperty("car_num")
|
||||
private int carNum;
|
||||
|
||||
private String name;
|
||||
private String team;
|
||||
|
||||
@JsonProperty("q1")
|
||||
private String q1Time;
|
||||
|
||||
@JsonProperty("q2")
|
||||
private String q2Time;
|
||||
|
||||
@JsonProperty("q3")
|
||||
private String q3Time;
|
||||
|
||||
public QualifyingResultItem(int position, int carNum, String name, String team, String q1Time, String q2Time,
|
||||
String q3Time) {
|
||||
this.position = position;
|
||||
this.carNum = carNum;
|
||||
this.name = name;
|
||||
this.team = team;
|
||||
this.q1Time = q1Time;
|
||||
this.q2Time = q2Time;
|
||||
this.q3Time = q3Time;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public int getCarNum() {
|
||||
return carNum;
|
||||
}
|
||||
|
||||
public void setCarNum(int carNum) {
|
||||
this.carNum = carNum;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getTeam() {
|
||||
return team;
|
||||
}
|
||||
|
||||
public void setTeam(String team) {
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
public String getQ1Time() {
|
||||
return q1Time;
|
||||
}
|
||||
|
||||
public void setQ1Time(String q1Time) {
|
||||
this.q1Time = q1Time;
|
||||
}
|
||||
|
||||
public String getQ2Time() {
|
||||
return q2Time;
|
||||
}
|
||||
|
||||
public void setQ2Time(String q2Time) {
|
||||
this.q2Time = q2Time;
|
||||
}
|
||||
|
||||
public String getQ3Time() {
|
||||
return q3Time;
|
||||
}
|
||||
|
||||
public void setQ3Time(String q3Time) {
|
||||
this.q3Time = q3Time;
|
||||
}
|
||||
}
|
||||
75
backend/src/main/java/f1/entity/RaceResultItem.java
Executable file
@@ -0,0 +1,75 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class RaceResultItem {
|
||||
private int pos;
|
||||
|
||||
@JsonProperty("car_num")
|
||||
private int carNum;
|
||||
|
||||
private String name;
|
||||
private String team;
|
||||
|
||||
@JsonProperty("finish_time")
|
||||
private String finishTime;
|
||||
|
||||
private int score;
|
||||
|
||||
public RaceResultItem(int pos, int carNum, String name, String team, String finishTime, int score) {
|
||||
this.pos = pos;
|
||||
this.carNum = carNum;
|
||||
this.name = name;
|
||||
this.team = team;
|
||||
this.finishTime = finishTime;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public int getPos() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
public void setPos(int pos) {
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
public int getCarNum() {
|
||||
return carNum;
|
||||
}
|
||||
|
||||
public void setCarNum(int carNum) {
|
||||
this.carNum = carNum;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getTeam() {
|
||||
return team;
|
||||
}
|
||||
|
||||
public void setTeam(String team) {
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
public String getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(String finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public void setScore(int score) {
|
||||
this.score = score;
|
||||
}
|
||||
}
|
||||
40
backend/src/main/java/f1/entity/SeasonDriver.java
Executable file
@@ -0,0 +1,40 @@
|
||||
package f1.entity;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
public class SeasonDriver extends Driver {
|
||||
private String team;
|
||||
private int carNum;
|
||||
private int season;
|
||||
|
||||
public SeasonDriver(int id, String name, String team, String country, Date birthday, int carNum, int season) {
|
||||
super(id, name, country, birthday);
|
||||
this.team = team;
|
||||
this.carNum = carNum;
|
||||
this.season = season;
|
||||
}
|
||||
|
||||
public String getTeam() {
|
||||
return team;
|
||||
}
|
||||
|
||||
public int getCarNum() {
|
||||
return carNum;
|
||||
}
|
||||
|
||||
public int getSeason() {
|
||||
return season;
|
||||
}
|
||||
|
||||
public void setTeam(String team) {
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
public void setCarNum(int carNum) {
|
||||
this.carNum = carNum;
|
||||
}
|
||||
|
||||
public void setSeason(int season) {
|
||||
this.season = season;
|
||||
}
|
||||
}
|
||||
85
backend/src/main/java/f1/entity/SeasonScheduleItem.java
Executable file
@@ -0,0 +1,85 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class SeasonScheduleItem {
|
||||
private int id;
|
||||
private int round;
|
||||
private String name;
|
||||
|
||||
@JsonProperty("have_sprint")
|
||||
private boolean haveSprint;
|
||||
|
||||
@JsonProperty("circuit_id")
|
||||
private int circuitId;
|
||||
|
||||
private String country;
|
||||
private String location;
|
||||
|
||||
public SeasonScheduleItem(int id, int round, String name, boolean haveSprint, int circuitId, String country,
|
||||
String location) {
|
||||
this.id = id;
|
||||
this.round = round;
|
||||
this.name = name;
|
||||
this.haveSprint = haveSprint;
|
||||
this.circuitId = circuitId;
|
||||
this.country = country;
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getRound() {
|
||||
return round;
|
||||
}
|
||||
|
||||
public void setRound(int round) {
|
||||
this.round = round;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean isHaveSprint() {
|
||||
return haveSprint;
|
||||
}
|
||||
|
||||
public void setHaveSprint(boolean haveSprint) {
|
||||
this.haveSprint = haveSprint;
|
||||
}
|
||||
|
||||
public int getCircuitId() {
|
||||
return circuitId;
|
||||
}
|
||||
|
||||
public void setCircuitId(int circuitId) {
|
||||
this.circuitId = circuitId;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
}
|
||||
59
backend/src/main/java/f1/entity/Team.java
Executable file
@@ -0,0 +1,59 @@
|
||||
package f1.entity;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
public class Team {
|
||||
private int id;
|
||||
private String name;
|
||||
private String country;
|
||||
private String engineSupplier;
|
||||
private Date setupTime;
|
||||
|
||||
public Team(int id, String name, String country, String engineSupplier, Date setupTime) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.country = country;
|
||||
this.engineSupplier = engineSupplier;
|
||||
this.setupTime = setupTime;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public String getEngineSupplier() {
|
||||
return engineSupplier;
|
||||
}
|
||||
|
||||
public Date getSetupTime() {
|
||||
return setupTime;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public void setEngineSupplier(String engineSupplier) {
|
||||
this.engineSupplier = engineSupplier;
|
||||
}
|
||||
|
||||
public void setSetupTime(Date setupTime) {
|
||||
this.setupTime = setupTime;
|
||||
}
|
||||
}
|
||||
24
backend/src/main/java/f1/entity/TeamHistoryItem.java
Executable file
@@ -0,0 +1,24 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TeamHistoryItem {
|
||||
@JsonProperty("prix_name")
|
||||
private String prixName;
|
||||
|
||||
@JsonProperty("total_score")
|
||||
private int totalScore;
|
||||
|
||||
public TeamHistoryItem(String prixName, int totalScore) {
|
||||
this.prixName = prixName;
|
||||
this.totalScore = totalScore;
|
||||
}
|
||||
|
||||
public String getPrixName() {
|
||||
return prixName;
|
||||
}
|
||||
|
||||
public int getTotalScore() {
|
||||
return totalScore;
|
||||
}
|
||||
}
|
||||
64
backend/src/main/java/f1/entity/TeamStatistic.java
Executable file
@@ -0,0 +1,64 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TeamStatistic {
|
||||
private DriverStatistic formal;
|
||||
private DriverStatistic sprint;
|
||||
private List<String> drivers;
|
||||
private int rank;
|
||||
|
||||
@JsonProperty("total_score")
|
||||
private int totalScore;
|
||||
|
||||
public TeamStatistic(DriverStatistic formal, DriverStatistic sprint, List<String> drivers, int rank,
|
||||
int totalScore) {
|
||||
this.formal = formal;
|
||||
this.sprint = sprint;
|
||||
this.drivers = drivers;
|
||||
this.rank = rank;
|
||||
this.totalScore = totalScore;
|
||||
}
|
||||
|
||||
public DriverStatistic getFormal() {
|
||||
return formal;
|
||||
}
|
||||
|
||||
public void setFormal(DriverStatistic formal) {
|
||||
this.formal = formal;
|
||||
}
|
||||
|
||||
public DriverStatistic getSprint() {
|
||||
return sprint;
|
||||
}
|
||||
|
||||
public void setSprint(DriverStatistic sprint) {
|
||||
this.sprint = sprint;
|
||||
}
|
||||
|
||||
public List<String> getDrivers() {
|
||||
return drivers;
|
||||
}
|
||||
|
||||
public void setDrivers(List<String> drivers) {
|
||||
this.drivers = drivers;
|
||||
}
|
||||
|
||||
public int getRank() {
|
||||
return rank;
|
||||
}
|
||||
|
||||
public void setRank(int rank) {
|
||||
this.rank = rank;
|
||||
}
|
||||
|
||||
public int getTotalScore() {
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
public void setTotalScore(int totalScore) {
|
||||
this.totalScore = totalScore;
|
||||
}
|
||||
}
|
||||
73
backend/src/main/java/f1/entity/User.java
Executable file
@@ -0,0 +1,73 @@
|
||||
package f1.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public class User {
|
||||
private int id;
|
||||
private String username;
|
||||
@JsonIgnore
|
||||
private String password;
|
||||
private String email;
|
||||
private String country;
|
||||
private String avatar;
|
||||
|
||||
public User() {
|
||||
}
|
||||
|
||||
public User(int id, String username, String password, String email, String country, String avatar) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
this.country = country;
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(String avatar) {
|
||||
this.avatar = avatar;
|
||||
}
|
||||
}
|
||||
50
backend/src/main/java/f1/security/JwtFilter.java
Executable file
@@ -0,0 +1,50 @@
|
||||
package f1.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Component
|
||||
public class JwtFilter extends OncePerRequestFilter {
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String token = null;
|
||||
if (request.getCookies() != null) {
|
||||
for (Cookie cookie : request.getCookies()) {
|
||||
if ("auth_token".equals(cookie.getName())) {
|
||||
token = cookie.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (token != null && jwtUtil.validateToken(token)) {
|
||||
String username = jwtUtil.getUsernameFromToken(token);
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
username, null, new ArrayList<>());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
56
backend/src/main/java/f1/security/JwtUtil.java
Executable file
@@ -0,0 +1,56 @@
|
||||
package f1.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secretKeyString;
|
||||
|
||||
private Key secretKey;
|
||||
private static final long EXPIRATION_TIME = 86400000; // 24小时 (毫秒)
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
byte[] keyBytes = Decoders.BASE64.decode(secretKeyString);
|
||||
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
public String generateToken(String username) {
|
||||
return Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
|
||||
.signWith(secretKey, SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String getUsernameFromToken(String token) {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
backend/src/main/java/f1/security/SecurityConfig.java
Executable file
@@ -0,0 +1,39 @@
|
||||
package f1.security;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Autowired
|
||||
private JwtFilter jwtFilter;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf().disable()
|
||||
.authorizeHttpRequests()
|
||||
// Blacklist: Protect specific endpoints
|
||||
.requestMatchers(HttpMethod.GET, "/api/user").authenticated()
|
||||
.requestMatchers(HttpMethod.POST, "/api/comments/**").authenticated()
|
||||
.requestMatchers(HttpMethod.DELETE, "/api/comments/**").authenticated()
|
||||
// Whitelist: Allow everything else (static resources, other APIs)
|
||||
.anyRequest().permitAll()
|
||||
.and()
|
||||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
4
backend/src/main/resources/application.properties
Executable file
@@ -0,0 +1,4 @@
|
||||
spring.datasource.url=jdbc:mysql://124.70.86.207:3306/h_db23373332
|
||||
spring.datasource.username=u23373332
|
||||
spring.datasource.password=
|
||||
jwt.secret=
|
||||
6
backend/src/main/resources/application.properties.example
Executable file
@@ -0,0 +1,6 @@
|
||||
spring.datasource.url=jdbc:mysql://your-host:3306/your-db
|
||||
spring.datasource.username=your-username
|
||||
spring.datasource.password=your-password
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
jwt.secret=24VSSNj6bDX7mwjIsidnJQYg3Lk6Ht9YvqYZYj7aDTc=
|
||||
jwt.secret=your-base64-encoded-secret-key-min-32-bytes
|
||||
148
backend/src/main/resources/init.sql
Executable file
@@ -0,0 +1,148 @@
|
||||
-- 车手表
|
||||
CREATE TABLE IF NOT EXISTS driver(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
birthday DATE
|
||||
);
|
||||
|
||||
-- 车队表
|
||||
CREATE TABLE IF NOT EXISTS team(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
engine_supplier VARCHAR(100),
|
||||
setup_time YEAR
|
||||
);
|
||||
|
||||
-- 合同表
|
||||
CREATE TABLE IF NOT EXISTS contract(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
driver_id INT,
|
||||
team_id INT,
|
||||
season YEAR,
|
||||
car_num INT,
|
||||
is_reserve BOOLEAN,
|
||||
FOREIGN KEY(driver_id) REFERENCES driver(id),
|
||||
FOREIGN KEY(team_id) REFERENCES team(id)
|
||||
);
|
||||
|
||||
-- 赛道表
|
||||
CREATE TABLE IF NOT EXISTS circuit(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
location VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
drs_zones INT
|
||||
);
|
||||
|
||||
-- 赛事表
|
||||
CREATE TABLE IF NOT EXISTS prix(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
season YEAR,
|
||||
round INT,
|
||||
name VARCHAR(100),
|
||||
circuit_id INT,
|
||||
have_sprint BOOLEAN,
|
||||
FOREIGN KEY(circuit_id) REFERENCES circuit(id)
|
||||
);
|
||||
|
||||
-- 排位赛结果表
|
||||
CREATE TABLE IF NOT EXISTS qualifying_result(
|
||||
prix_id INT,
|
||||
driver_id INT,
|
||||
team_id INT,
|
||||
fastest_time VARCHAR(20),
|
||||
position INT,
|
||||
is_sprint BOOLEAN,
|
||||
section INT,
|
||||
FOREIGN KEY(prix_id) REFERENCES prix(id),
|
||||
FOREIGN KEY(driver_id) REFERENCES driver(id),
|
||||
FOREIGN KEY(team_id) REFERENCES team(id)
|
||||
);
|
||||
|
||||
-- 正赛结果表
|
||||
CREATE TABLE IF NOT EXISTS race_result(
|
||||
prix_id INT,
|
||||
driver_id INT,
|
||||
team_id INT,
|
||||
start_position INT,
|
||||
end_position INT,
|
||||
finish_time VARCHAR(20),
|
||||
fastest_time VARCHAR(20),
|
||||
score INT,
|
||||
is_sprint BOOLEAN,
|
||||
FOREIGN KEY(prix_id) REFERENCES prix(id),
|
||||
FOREIGN KEY(driver_id) REFERENCES driver(id),
|
||||
FOREIGN KEY(team_id) REFERENCES team(id)
|
||||
);
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS user(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE,
|
||||
password VARCHAR(100),
|
||||
email VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
avatar VARCHAR(500)
|
||||
);
|
||||
|
||||
-- 评论表
|
||||
CREATE TABLE IF NOT EXISTS comment(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
response_id INT,
|
||||
root_id INT,
|
||||
content VARCHAR(500),
|
||||
FOREIGN KEY(user_id) REFERENCES user(id),
|
||||
FOREIGN KEY(response_id) REFERENCES comment(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(root_id) REFERENCES comment(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 赛季车手信息视图
|
||||
CREATE OR REPLACE VIEW season_driver(
|
||||
id, name, team, country, birthday, car_num, season
|
||||
) AS
|
||||
SELECT D.id,
|
||||
D.name,
|
||||
T.name,
|
||||
D.country,
|
||||
D.birthday,
|
||||
C.car_num,
|
||||
C.season
|
||||
FROM contract C
|
||||
JOIN team T ON T.id = C.team_id
|
||||
JOIN driver D ON C.driver_id = D.id;
|
||||
|
||||
-- 赛季车队积分榜视图
|
||||
CREATE OR REPLACE VIEW season_team_standings AS
|
||||
SELECT
|
||||
p.season,
|
||||
rr.team_id,
|
||||
SUM(rr.score) AS total_score,
|
||||
RANK() OVER (PARTITION BY p.season ORDER BY SUM(rr.score) DESC) AS ranking
|
||||
FROM race_result rr
|
||||
JOIN prix p ON rr.prix_id = p.id
|
||||
GROUP BY p.season, rr.team_id;
|
||||
|
||||
CREATE OR REPLACE VIEW prix_result AS
|
||||
SELECT
|
||||
p.season,
|
||||
p.id AS prix_id,
|
||||
rr.driver_id,
|
||||
rr.team_id,
|
||||
p.name AS prix_name,
|
||||
p.round,
|
||||
sd.name AS driver_name,
|
||||
sd.car_num,
|
||||
sd.team AS team_name,
|
||||
rr.end_position AS pos,
|
||||
rr.finish_time,
|
||||
rr.score,
|
||||
rr.is_sprint
|
||||
FROM
|
||||
race_result rr
|
||||
JOIN
|
||||
prix p ON rr.prix_id = p.id
|
||||
JOIN
|
||||
season_driver sd ON rr.driver_id = sd.id AND sd.season = p.season;
|
||||
4
backend/target/classes/application.properties
Executable file
@@ -0,0 +1,4 @@
|
||||
spring.datasource.url=jdbc:mysql://124.70.86.207:3306/h_db23373332
|
||||
spring.datasource.username=u23373332
|
||||
spring.datasource.password=
|
||||
jwt.secret=
|
||||
6
backend/target/classes/application.properties.example
Executable file
@@ -0,0 +1,6 @@
|
||||
spring.datasource.url=jdbc:mysql://your-host:3306/your-db
|
||||
spring.datasource.username=your-username
|
||||
spring.datasource.password=your-password
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
jwt.secret=24VSSNj6bDX7mwjIsidnJQYg3Lk6Ht9YvqYZYj7aDTc=
|
||||
jwt.secret=your-base64-encoded-secret-key-min-32-bytes
|
||||
BIN
backend/target/classes/f1/AppConfig.class
Normal file
BIN
backend/target/classes/f1/Main.class
Normal file
BIN
backend/target/classes/f1/controller/AuthController.class
Normal file
BIN
backend/target/classes/f1/controller/CommentController.class
Normal file
BIN
backend/target/classes/f1/controller/F1Controller.class
Normal file
BIN
backend/target/classes/f1/controller/UserController.class
Normal file
BIN
backend/target/classes/f1/db/BaseDao.class
Normal file
BIN
backend/target/classes/f1/db/Database.class
Normal file
BIN
backend/target/classes/f1/db/DriverDao.class
Normal file
BIN
backend/target/classes/f1/db/RaceDao.class
Normal file
BIN
backend/target/classes/f1/db/TeamDao.class
Normal file
BIN
backend/target/classes/f1/db/UserDao.class
Normal file
BIN
backend/target/classes/f1/entity/CommentItem.class
Normal file
BIN
backend/target/classes/f1/entity/Driver.class
Normal file
BIN
backend/target/classes/f1/entity/DriverHistoryItem.class
Normal file
BIN
backend/target/classes/f1/entity/DriverStatistic.class
Normal file
BIN
backend/target/classes/f1/entity/QualifyingResultItem.class
Normal file
BIN
backend/target/classes/f1/entity/RaceResultItem.class
Normal file
BIN
backend/target/classes/f1/entity/SeasonDriver.class
Normal file
BIN
backend/target/classes/f1/entity/SeasonScheduleItem.class
Normal file
BIN
backend/target/classes/f1/entity/Team.class
Normal file
BIN
backend/target/classes/f1/entity/TeamHistoryItem.class
Normal file
BIN
backend/target/classes/f1/entity/TeamStatistic.class
Normal file
BIN
backend/target/classes/f1/entity/User.class
Normal file
BIN
backend/target/classes/f1/security/JwtFilter.class
Normal file
BIN
backend/target/classes/f1/security/JwtUtil.class
Normal file
BIN
backend/target/classes/f1/security/SecurityConfig.class
Normal file
148
backend/target/classes/init.sql
Executable file
@@ -0,0 +1,148 @@
|
||||
-- 车手表
|
||||
CREATE TABLE IF NOT EXISTS driver(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
birthday DATE
|
||||
);
|
||||
|
||||
-- 车队表
|
||||
CREATE TABLE IF NOT EXISTS team(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
engine_supplier VARCHAR(100),
|
||||
setup_time YEAR
|
||||
);
|
||||
|
||||
-- 合同表
|
||||
CREATE TABLE IF NOT EXISTS contract(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
driver_id INT,
|
||||
team_id INT,
|
||||
season YEAR,
|
||||
car_num INT,
|
||||
is_reserve BOOLEAN,
|
||||
FOREIGN KEY(driver_id) REFERENCES driver(id),
|
||||
FOREIGN KEY(team_id) REFERENCES team(id)
|
||||
);
|
||||
|
||||
-- 赛道表
|
||||
CREATE TABLE IF NOT EXISTS circuit(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
location VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
drs_zones INT
|
||||
);
|
||||
|
||||
-- 赛事表
|
||||
CREATE TABLE IF NOT EXISTS prix(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
season YEAR,
|
||||
round INT,
|
||||
name VARCHAR(100),
|
||||
circuit_id INT,
|
||||
have_sprint BOOLEAN,
|
||||
FOREIGN KEY(circuit_id) REFERENCES circuit(id)
|
||||
);
|
||||
|
||||
-- 排位赛结果表
|
||||
CREATE TABLE IF NOT EXISTS qualifying_result(
|
||||
prix_id INT,
|
||||
driver_id INT,
|
||||
team_id INT,
|
||||
fastest_time VARCHAR(20),
|
||||
position INT,
|
||||
is_sprint BOOLEAN,
|
||||
section INT,
|
||||
FOREIGN KEY(prix_id) REFERENCES prix(id),
|
||||
FOREIGN KEY(driver_id) REFERENCES driver(id),
|
||||
FOREIGN KEY(team_id) REFERENCES team(id)
|
||||
);
|
||||
|
||||
-- 正赛结果表
|
||||
CREATE TABLE IF NOT EXISTS race_result(
|
||||
prix_id INT,
|
||||
driver_id INT,
|
||||
team_id INT,
|
||||
start_position INT,
|
||||
end_position INT,
|
||||
finish_time VARCHAR(20),
|
||||
fastest_time VARCHAR(20),
|
||||
score INT,
|
||||
is_sprint BOOLEAN,
|
||||
FOREIGN KEY(prix_id) REFERENCES prix(id),
|
||||
FOREIGN KEY(driver_id) REFERENCES driver(id),
|
||||
FOREIGN KEY(team_id) REFERENCES team(id)
|
||||
);
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS user(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE,
|
||||
password VARCHAR(100),
|
||||
email VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
avatar VARCHAR(500)
|
||||
);
|
||||
|
||||
-- 评论表
|
||||
CREATE TABLE IF NOT EXISTS comment(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
response_id INT,
|
||||
root_id INT,
|
||||
content VARCHAR(500),
|
||||
FOREIGN KEY(user_id) REFERENCES user(id),
|
||||
FOREIGN KEY(response_id) REFERENCES comment(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(root_id) REFERENCES comment(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 赛季车手信息视图
|
||||
CREATE OR REPLACE VIEW season_driver(
|
||||
id, name, team, country, birthday, car_num, season
|
||||
) AS
|
||||
SELECT D.id,
|
||||
D.name,
|
||||
T.name,
|
||||
D.country,
|
||||
D.birthday,
|
||||
C.car_num,
|
||||
C.season
|
||||
FROM contract C
|
||||
JOIN team T ON T.id = C.team_id
|
||||
JOIN driver D ON C.driver_id = D.id;
|
||||
|
||||
-- 赛季车队积分榜视图
|
||||
CREATE OR REPLACE VIEW season_team_standings AS
|
||||
SELECT
|
||||
p.season,
|
||||
rr.team_id,
|
||||
SUM(rr.score) AS total_score,
|
||||
RANK() OVER (PARTITION BY p.season ORDER BY SUM(rr.score) DESC) AS ranking
|
||||
FROM race_result rr
|
||||
JOIN prix p ON rr.prix_id = p.id
|
||||
GROUP BY p.season, rr.team_id;
|
||||
|
||||
CREATE OR REPLACE VIEW prix_result AS
|
||||
SELECT
|
||||
p.season,
|
||||
p.id AS prix_id,
|
||||
rr.driver_id,
|
||||
rr.team_id,
|
||||
p.name AS prix_name,
|
||||
p.round,
|
||||
sd.name AS driver_name,
|
||||
sd.car_num,
|
||||
sd.team AS team_name,
|
||||
rr.end_position AS pos,
|
||||
rr.finish_time,
|
||||
rr.score,
|
||||
rr.is_sprint
|
||||
FROM
|
||||
race_result rr
|
||||
JOIN
|
||||
prix p ON rr.prix_id = p.id
|
||||
JOIN
|
||||
season_driver sd ON rr.driver_id = sd.id AND sd.season = p.season;
|
||||
@@ -0,0 +1,27 @@
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/Driver.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/QualifyingResultItem.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/UserController.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/Database.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/security/SecurityConfig.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/security/JwtFilter.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/Main.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/TeamDao.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/User.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/AppConfig.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/SeasonDriver.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/security/JwtUtil.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/CommentItem.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/RaceResultItem.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/AuthController.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/DriverStatistic.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/UserDao.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/TeamHistoryItem.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/Team.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/DriverDao.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/TeamStatistic.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/SeasonScheduleItem.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/entity/DriverHistoryItem.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/CommentController.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/RaceDao.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/controller/F1Controller.java
|
||||
/Users/colden/编程/web/formula1/backend/src/main/java/f1/db/BaseDao.java
|
||||
1
frontend/env.d.ts
vendored
Executable file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
frontend/index.html
Executable file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- <link rel="icon" href="/favicon.ico"> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
140
frontend/src/App.vue
Executable file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute, RouterView } from 'vue-router';
|
||||
import { clearToken, setSessionAuth } from './utils/auth';
|
||||
import { userInfoStore } from './store/UserInfo'
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userInfo = userInfoStore()
|
||||
|
||||
const handleNavClick = (path: string) => {
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
const mainContent = ref<HTMLElement | null>(null)
|
||||
router.afterEach(() => {
|
||||
if (mainContent.value) {
|
||||
mainContent.value.scrollTop = 0
|
||||
}
|
||||
})
|
||||
|
||||
const LogOut = async () => {
|
||||
router.push('/login');
|
||||
clearToken();
|
||||
setSessionAuth(false);
|
||||
await userInfo.clearUserInfo()
|
||||
}
|
||||
|
||||
const hideHeader = computed(() => route.name === 'login' || route.name === 'register')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="app">
|
||||
<el-header v-if="!hideHeader" class="header">
|
||||
<div class="navbar" :router="true">
|
||||
<div @click="handleNavClick('/')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-home"></use>
|
||||
</svg>Home
|
||||
</div>
|
||||
<div @click="handleNavClick('/seasons')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-milestone"></use>
|
||||
</svg>Seasons
|
||||
</div>
|
||||
<div @click="handleNavClick('/teams')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-car"></use>
|
||||
</svg>Teams
|
||||
</div>
|
||||
<div @click="handleNavClick('/drivers')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-driver"></use>
|
||||
</svg>Drivers
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<!-- <span>
|
||||
<img src="@/assets/logo.webp" alt="logo">
|
||||
</span> -->
|
||||
<el-avatar :size="32" class="avatar">{{ userInfo.userInfo.username?.[0] || '?' }}</el-avatar>
|
||||
<el-button type="primary" @click="LogOut()" class="logOut">LogOut</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main :class="hideHeader ? 'main-no-header' : 'main'" ref="mainContent">
|
||||
<RouterView />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 90px;
|
||||
background: linear-gradient(90deg, #f00f0f 0%, #ee8d11 25%, #c026d9 50%, #294fd6 75%, #2fd03f 100%);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.navbar div {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.navbar div:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgb(198, 230, 239, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-top: 90px;
|
||||
padding: 0;
|
||||
background-image: url('@/assets/far.jpg');
|
||||
background-size: cover;
|
||||
height: calc(100vh - 90px);
|
||||
&>*{
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.main-no-header {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.logOut {
|
||||
margin: auto 0;
|
||||
margin-left: 10px;
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
38
frontend/src/api/comments.ts
Executable file
@@ -0,0 +1,38 @@
|
||||
import { apiJSON } from './http'
|
||||
|
||||
export interface CommentItem {
|
||||
id: number
|
||||
username: string
|
||||
content: string
|
||||
user_id: number
|
||||
parentId?: number
|
||||
toUser?: string
|
||||
}
|
||||
|
||||
export interface CommentListResp {
|
||||
items: CommentItem[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export const fetchLatestComments = (page: number, pageSize: number) => {
|
||||
const offset = (page - 1) * pageSize
|
||||
return apiJSON<any>(`/api/comments?limit=100&offset=${offset}&pageSize=${pageSize}`)
|
||||
}
|
||||
|
||||
export const postComment = (user_id: number, content: string) => {
|
||||
return apiJSON<{}>('/api/comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ user_id, content })
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchCommentThread = (id: number) => {
|
||||
return apiJSON<any>(`/api/comments/${id}/replies`)
|
||||
}
|
||||
|
||||
export const postReply = (rootId: number, content: string, user_id: number, response_id: number = -1) => {
|
||||
return apiJSON<{}>(`/api/comments/${rootId}/replies`, {
|
||||
method: 'POST',
|
||||
body: response_id === -1 ? JSON.stringify({ user_id, content }) : JSON.stringify({ user_id, content, response_id })
|
||||
})
|
||||
}
|
||||
27
frontend/src/api/http.ts
Executable file
@@ -0,0 +1,27 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export async function apiFetch(path: string, options: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
}
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
const res = await fetch(path, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export async function apiJSON<T>(path: string, options: RequestInit = {}) {
|
||||
const res = await apiFetch(path, options)
|
||||
if (!res.ok) throw new Error(`请求失败: ${res.status}`)
|
||||
const ct = res.headers.get('content-type') || ''
|
||||
if (ct.includes('application/json')) return res.json() as Promise<T>
|
||||
const text = await res.text()
|
||||
// 文本成功兼容
|
||||
if (/success/i.test(text)) return ({ success: true } as unknown) as T
|
||||
throw new Error(text || '请求失败')
|
||||
}
|
||||
BIN
frontend/src/assets/.DS_Store
vendored
Executable file
BIN
frontend/src/assets/Formula1-Black.woff2
Executable file
BIN
frontend/src/assets/drivers/alb.avif
Executable file
|
After Width: | Height: | Size: 42 KiB |
BIN
frontend/src/assets/drivers/alo.avif
Executable file
|
After Width: | Height: | Size: 45 KiB |
BIN
frontend/src/assets/drivers/ant.avif
Executable file
|
After Width: | Height: | Size: 51 KiB |
BIN
frontend/src/assets/drivers/ber.avif
Executable file
|
After Width: | Height: | Size: 37 KiB |
BIN
frontend/src/assets/drivers/bor.avif
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/src/assets/drivers/col.avif
Executable file
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/src/assets/drivers/gas.avif
Executable file
|
After Width: | Height: | Size: 45 KiB |
BIN
frontend/src/assets/drivers/haj.avif
Executable file
|
After Width: | Height: | Size: 47 KiB |
BIN
frontend/src/assets/drivers/ham.avif
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/src/assets/drivers/hul.avif
Executable file
|
After Width: | Height: | Size: 41 KiB |
BIN
frontend/src/assets/drivers/lando.avif
Executable file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/src/assets/drivers/law.avif
Executable file
|
After Width: | Height: | Size: 49 KiB |
BIN
frontend/src/assets/drivers/lec.avif
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/src/assets/drivers/max.avif
Executable file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/src/assets/drivers/oc.avif
Executable file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/src/assets/drivers/piastri.avif
Executable file
|
After Width: | Height: | Size: 82 KiB |
BIN
frontend/src/assets/drivers/russell.avif
Executable file
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/src/assets/drivers/sai.avif
Executable file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/src/assets/drivers/str.avif
Executable file
|
After Width: | Height: | Size: 49 KiB |
BIN
frontend/src/assets/drivers/yuki.avif
Executable file
|
After Width: | Height: | Size: 132 KiB |
BIN
frontend/src/assets/far.jpg
Executable file
|
After Width: | Height: | Size: 739 KiB |
BIN
frontend/src/assets/heying.jpg
Executable file
|
After Width: | Height: | Size: 209 KiB |
539
frontend/src/assets/icon/font/demo.css
Executable file
@@ -0,0 +1,539 @@
|
||||
/* Logo 字体 */
|
||||
@font-face {
|
||||
font-family: "iconfont logo";
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "iconfont logo";
|
||||
font-size: 160px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-more {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
#tabs .active {
|
||||
border-bottom-color: #f00;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tab-container .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.main {
|
||||
padding: 30px 100px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main .logo {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1;
|
||||
height: 110px;
|
||||
margin-top: -50px;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
font-size: 160px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.helps {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.helps pre {
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #e7e1cd;
|
||||
background-color: #fffdef;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon_lists {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.icon_lists li {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
list-style: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon_lists li .code-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon_lists .icon {
|
||||
display: block;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
font-size: 42px;
|
||||
margin: 10px auto;
|
||||
color: #333;
|
||||
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
-moz-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
transition: font-size 0.25s linear, width 0.25s linear;
|
||||
}
|
||||
|
||||
.icon_lists .icon:hover {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.icon_lists .svg-icon {
|
||||
/* 通过设置 font-size 来改变图标大小 */
|
||||
width: 1em;
|
||||
/* 图标和文字相邻时,垂直对齐 */
|
||||
vertical-align: -0.15em;
|
||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
||||
fill: currentColor;
|
||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
||||
normalize.css 中也包含这行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon_lists li .name,
|
||||
.icon_lists li .code-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* markdown 样式 */
|
||||
.markdown {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
color: #404040;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
color: #404040;
|
||||
margin: 1.6em 0 0.6em 0;
|
||||
font-weight: 500;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #e9e9e9;
|
||||
margin: 16px 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown>p,
|
||||
.markdown>blockquote,
|
||||
.markdown>.highlight,
|
||||
.markdown>ol,
|
||||
.markdown>ul {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.markdown ul>li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.markdown>ul li,
|
||||
.markdown blockquote ul>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown>ul li p,
|
||||
.markdown>ol li p {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown ol>li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown>ol li,
|
||||
.markdown blockquote ol>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
margin: 0 3px;
|
||||
padding: 0 5px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown strong,
|
||||
.markdown b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
empty-cells: show;
|
||||
border: 1px solid #e9e9e9;
|
||||
width: 95%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table th,
|
||||
.markdown>table td {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
border-left: 4px solid #e9e9e9;
|
||||
padding-left: 0.8em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown .anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.markdown .waiting {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.markdown h1:hover .anchor,
|
||||
.markdown h2:hover .anchor,
|
||||
.markdown h3:hover .anchor,
|
||||
.markdown h4:hover .anchor,
|
||||
.markdown h5:hover .anchor,
|
||||
.markdown h6:hover .anchor {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown>br,
|
||||
.markdown>p>br {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
/* PrismJS 1.15.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre)>code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre)>code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
303
frontend/src/assets/icon/font/demo_index.html
Executable file
@@ -0,0 +1,303 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>iconfont Demo</title>
|
||||
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
|
||||
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<link rel="stylesheet" href="iconfont.css">
|
||||
<script src="iconfont.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
|
||||
<!-- 代码高亮 -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
|
||||
<style>
|
||||
.main .logo {
|
||||
margin-top: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main .logo .sub-title {
|
||||
margin-left: 0.5em;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
background: linear-gradient(-45deg, #3967FF, #B500FE);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
|
||||
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
|
||||
|
||||
</a></h1>
|
||||
<div class="nav-tabs">
|
||||
<ul id="tabs" class="dib-box">
|
||||
<li class="dib active"><span>Unicode</span></li>
|
||||
<li class="dib"><span>Font class</span></li>
|
||||
<li class="dib"><span>Symbol</span></li>
|
||||
</ul>
|
||||
|
||||
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=5075080" target="_blank" class="nav-more">查看项目</a>
|
||||
|
||||
</div>
|
||||
<div class="tab-container">
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cup</div>
|
||||
<div class="code-name">&#xe637;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">race car</div>
|
||||
<div class="code-name">&#xe600;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">sports_icon_racing car@2x</div>
|
||||
<div class="code-name">&#xe67b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">Home, homepage, menu</div>
|
||||
<div class="code-name">&#xe9db;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">icon_task_details_milestone</div>
|
||||
<div class="code-name">&#xe6c4;</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="unicode-">Unicode 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
|
||||
<ul>
|
||||
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
|
||||
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
|
||||
</blockquote>
|
||||
<p>Unicode 使用步骤如下:</p>
|
||||
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1764608411765') format('woff2'),
|
||||
url('iconfont.woff?t=1764608411765') format('woff'),
|
||||
url('iconfont.ttf?t=1764608411765') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
<pre><code class="language-css"
|
||||
>.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
|
||||
<pre>
|
||||
<code class="language-html"
|
||||
><span class="iconfont">&#x33;</span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-cup"></span>
|
||||
<div class="name">
|
||||
cup
|
||||
</div>
|
||||
<div class="code-name">.icon-cup
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-car"></span>
|
||||
<div class="name">
|
||||
race car
|
||||
</div>
|
||||
<div class="code-name">.icon-car
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-driver"></span>
|
||||
<div class="name">
|
||||
sports_icon_racing car@2x
|
||||
</div>
|
||||
<div class="code-name">.icon-driver
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-home"></span>
|
||||
<div class="name">
|
||||
Home, homepage, menu
|
||||
</div>
|
||||
<div class="code-name">.icon-home
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-milestone"></span>
|
||||
<div class="name">
|
||||
icon_task_details_milestone
|
||||
</div>
|
||||
<div class="code-name">.icon-milestone
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="font-class-">font-class 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
|
||||
<p>与 Unicode 使用方式相比,具有如下特点:</p>
|
||||
<ul>
|
||||
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
|
||||
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
|
||||
<pre><code class="language-html"><link rel="stylesheet" href="./iconfont.css">
|
||||
</code></pre>
|
||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"
|
||||
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-cup"></use>
|
||||
</svg>
|
||||
<div class="name">cup</div>
|
||||
<div class="code-name">#icon-cup</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-car"></use>
|
||||
</svg>
|
||||
<div class="name">race car</div>
|
||||
<div class="code-name">#icon-car</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-driver"></use>
|
||||
</svg>
|
||||
<div class="name">sports_icon_racing car@2x</div>
|
||||
<div class="code-name">#icon-driver</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-home"></use>
|
||||
</svg>
|
||||
<div class="name">Home, homepage, menu</div>
|
||||
<div class="code-name">#icon-home</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-milestone"></use>
|
||||
</svg>
|
||||
<div class="name">icon_task_details_milestone</div>
|
||||
<div class="code-name">#icon-milestone</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="symbol-">Symbol 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
|
||||
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
|
||||
<ul>
|
||||
<li>支持多色图标了,不再受单色限制。</li>
|
||||
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
|
||||
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
|
||||
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
|
||||
<pre><code class="language-html"><script src="./iconfont.js"></script>
|
||||
</code></pre>
|
||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
||||
<pre><code class="language-html"><style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xxx"></use>
|
||||
</svg>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.tab-container .content:first').show()
|
||||
|
||||
$('#tabs li').click(function (e) {
|
||||
var tabContent = $('.tab-container .content')
|
||||
var index = $(this).index()
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
return
|
||||
} else {
|
||||
$('#tabs li').removeClass('active')
|
||||
$(this).addClass('active')
|
||||
|
||||
tabContent.hide().eq(index).fadeIn()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
35
frontend/src/assets/icon/font/iconfont.css
Executable file
@@ -0,0 +1,35 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 5075080 */
|
||||
src: url('iconfont.woff2?t=1764608411765') format('woff2'),
|
||||
url('iconfont.woff?t=1764608411765') format('woff'),
|
||||
url('iconfont.ttf?t=1764608411765') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-cup:before {
|
||||
content: "\e637";
|
||||
}
|
||||
|
||||
.icon-car:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
||||
.icon-driver:before {
|
||||
content: "\e67b";
|
||||
}
|
||||
|
||||
.icon-home:before {
|
||||
content: "\e9db";
|
||||
}
|
||||
|
||||
.icon-milestone:before {
|
||||
content: "\e6c4";
|
||||
}
|
||||
|
||||
1
frontend/src/assets/icon/font/iconfont.js
Executable file
44
frontend/src/assets/icon/font/iconfont.json
Executable file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "5075080",
|
||||
"name": "f1",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "1206006",
|
||||
"name": "cup",
|
||||
"font_class": "cup",
|
||||
"unicode": "e637",
|
||||
"unicode_decimal": 58935
|
||||
},
|
||||
{
|
||||
"icon_id": "3900649",
|
||||
"name": "race car",
|
||||
"font_class": "car",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "9936992",
|
||||
"name": "sports_icon_racing car@2x",
|
||||
"font_class": "driver",
|
||||
"unicode": "e67b",
|
||||
"unicode_decimal": 59003
|
||||
},
|
||||
{
|
||||
"icon_id": "11982742",
|
||||
"name": "Home, homepage, menu",
|
||||
"font_class": "home",
|
||||
"unicode": "e9db",
|
||||
"unicode_decimal": 59867
|
||||
},
|
||||
{
|
||||
"icon_id": "13570233",
|
||||
"name": "icon_task_details_milestone",
|
||||
"font_class": "milestone",
|
||||
"unicode": "e6c4",
|
||||
"unicode_decimal": 59076
|
||||
}
|
||||
]
|
||||
}
|
||||