formula project

This commit is contained in:
colden
2025-12-20 12:20:43 +08:00
commit 28e1507889
156 changed files with 7444 additions and 0 deletions

110
backend/pom.xml Executable file
View 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>

View 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);
}
}

View 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();
}
}

View 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");
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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();
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}
}

View 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();
}
}

View 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=

View 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

View 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;

View 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=

View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

148
backend/target/classes/init.sql Executable file
View 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;

View File

@@ -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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

13
frontend/index.html Executable file
View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
frontend/src/assets/far.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

BIN
frontend/src/assets/heying.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View 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;
}

View 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">&#xe637;</span>
<div class="name">cup</div>
<div class="code-name">&amp;#xe637;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe600;</span>
<div class="name">race car</div>
<div class="code-name">&amp;#xe600;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe67b;</span>
<div class="name">sports_icon_racing car@2x</div>
<div class="code-name">&amp;#xe67b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9db;</span>
<div class="name">Home, homepage, menu</div>
<div class="code-name">&amp;#xe9db;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe6c4;</span>
<div class="name">icon_task_details_milestone</div>
<div class="code-name">&amp;#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"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</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">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</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">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</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>

View 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";
}

File diff suppressed because one or more lines are too long

View 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
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More