сделал 4 лабу

This commit is contained in:
2025-11-14 14:25:28 +04:00
parent 34fe6a28d9
commit 49510433e4
29 changed files with 18791 additions and 2350 deletions

Binary file not shown.

BIN
.vs/ипРома/v17/.wsuo Normal file

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\busla\\Desktop\\\u0438\u043F\u0420\u043E\u043C\u0430\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": []
}
]
}

View File

@@ -0,0 +1,12 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\busla\\Desktop\\\u0438\u043F\u0420\u043E\u043C\u0430\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": []
}
]
}

View File

@@ -0,0 +1,35 @@
apply plugin: "com.github.node-gradle.node"
logger.quiet("Configure front builder")
ext {
frontDir = file("${project.projectDir}/front")
logger.quiet("Webapp dir is ${frontDir}")
}
node {
version = "22.17.1"
npmVersion = "10.9.2"
download = true
}
tasks.register("frontDepsInstall", NpmTask) {
group = "front"
description = "Installs dependencies from package.json"
logger.quiet(description)
workingDir = frontDir
args = ["install"]
}
tasks.register("frontBuild", NpmTask) {
group = "front"
description = "Build frontend webapp"
logger.quiet(description)
workingDir = frontDir
dependsOn frontDepsInstall
args = ["run", "build"]
}
if (frontDir.exists()) {
processResources.finalizedBy frontBuild
}

View File

@@ -2,7 +2,10 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.5.5'
id 'io.spring.dependency-management' version '1.1.7'
id "com.github.node-gradle.node" version "7.1.0" apply false
id 'org.liquibase.gradle' version '2.2.0'
}
group = 'ru.ulstu.is'
version = '0.0.1-SNAPSHOT'
description = 'Movie API (Lab 1)'
@@ -18,7 +21,49 @@ java {
repositories { mavenCentral() }
ext {
springdocVersion = '2.8.11'
springProfiles = []
springdocVersion = '2.8.14'
springProfiles = []
if (project.hasProperty("front")) {
springProfiles.add("front")
}
if (project.hasProperty("prod")) {
springProfiles.add("prod")
} else {
springProfiles.add("dev")
}
currentProfiles = springProfiles.join(",")
logger.quiet("Current profiles are: " + currentProfiles)
}
liquibase {
activities {
dev {
url 'jdbc:h2:file:./data/appdb'
username 'sa'
driver 'org.h2.Driver'
changelogFile 'src/main/resources/db/changelog/db.changelog-master.yaml'
}
prod {
url 'jdbc:postgresql://127.0.0.1:5432/internet_lab4'
username 'postgres'
password 'postgres'
driver 'org.postgresql.Driver'
changelogFile 'src/main/resources/db/changelog/db.changelog-master.yaml'
}
}
runList = project.hasProperty('prod') ? 'prod' : 'dev'
}
configurations {
liquibaseRuntime {
extendsFrom runtimeClasspath
}
}
dependencies {
@@ -26,10 +71,44 @@ dependencies {
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.liquibase:liquibase-core:4.30.0'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
liquibaseRuntime 'org.liquibase:liquibase-core:4.30.0'
liquibaseRuntime 'info.picocli:picocli:4.7.6'
liquibaseRuntime 'com.h2database:h2:2.2.224'
if (springProfiles.contains("prod")) {
runtimeOnly "org.postgresql:postgresql:42.7.4"
} else {
runtimeOnly "org.postgresql:postgresql:42.7.4"
runtimeOnly "com.h2database:h2:2.2.224"
}
}
tasks.named('test') {
useJUnitPlatform()
}
if (springProfiles.contains("front")) {
apply from: "build.front.gradle"
}
bootRun {
def currentArgs = ["--spring.profiles.active=" + currentProfiles]
if (project.hasProperty("args")) {
currentArgs.addAll(project.args.tokenize())
}
args currentArgs
}
test {
useJUnitPlatform()
systemProperty "spring.profiles.active", currentProfiles
}

Binary file not shown.

View File

@@ -0,0 +1,12 @@
2025-11-14 13:30:14.250845+04:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-224]
2025-11-14 14:12:48.415553+04:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DIRECTORS" уже существует
Table "DIRECTORS" already exists; SQL statement:
CREATE TABLE PUBLIC.DIRECTORS (ID BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, NAME VARCHAR(255) NOT NULL, CONSTRAINT CONSTRAINT_6 PRIMARY KEY (ID)) [42101-224]
2025-11-14 14:17:18.481717+04:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-224]

6
backend/package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "backend",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -6,6 +6,10 @@ import org.springframework.web.bind.annotation.*;
import ru.ulstu.is.server.dto.MovieRq;
import ru.ulstu.is.server.dto.MovieRs;
import ru.ulstu.is.server.service.MovieService;
import jakarta.validation.constraints.Min;
import org.springframework.data.domain.Pageable;
import ru.ulstu.is.server.dto.PageHelper;
import ru.ulstu.is.server.dto.PageRs;
import java.util.List;
@@ -30,10 +34,11 @@ public class MovieController {
public MovieRs update(@PathVariable Long id, @RequestBody @Valid MovieRq rq) { return service.update(id, rq); }
@GetMapping("/page")
public List<MovieRs> getPage(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return service.getPage(page, size);
public PageRs<MovieRs> getPage(
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(defaultValue = "12") @Min(1) int size) {
Pageable pageable = PageHelper.toPageable(page, size);
return service.getPage(pageable);
}
@DeleteMapping("/{id}")

View File

@@ -0,0 +1,23 @@
package ru.ulstu.is.server.api;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SpaController {
@GetMapping("/")
public String index() {
return "forward:/index.html";
}
@GetMapping("/{path:^(?!api$)(?!v3$)(?!swagger-ui$).*$}")
public String any1() {
return "forward:/index.html";
}
@GetMapping("/{path:^(?!api$)(?!v3$)(?!swagger-ui$).*$}/**")
public String any2() {
return "forward:/index.html";
}
}

View File

@@ -0,0 +1,15 @@
package ru.ulstu.is.server.dto;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public final class PageHelper {
private PageHelper() {
}
public static Pageable toPageable(int page, int size) {
return PageRequest.of(page - 1, size, Sort.by("id"));
}
}

View File

@@ -0,0 +1,34 @@
package ru.ulstu.is.server.dto;
import org.springframework.data.domain.Page;
import java.util.List;
import java.util.function.Function;
public record PageRs<D>(
List<D> items,
int itemsCount,
int currentPage,
int currentSize,
int totalPages,
long totalItems,
boolean isFirst,
boolean isLast,
boolean hasNext,
boolean hasPrevious
) {
public static <D, E> PageRs<D> from(Page<E> page, Function<E, D> mapper) {
return new PageRs<>(
page.getContent().stream().map(mapper).toList(),
page.getNumberOfElements(),
page.getNumber() + 1,
page.getSize(),
page.getTotalPages(),
page.getTotalElements(),
page.isFirst(),
page.isLast(),
page.hasNext(),
page.hasPrevious()
);
}
}

View File

@@ -13,6 +13,9 @@ import ru.ulstu.is.server.dto.MovieRq;
import ru.ulstu.is.server.dto.MovieRs;
import ru.ulstu.is.server.entity.Director;
import ru.ulstu.is.server.entity.Genre;
import org.springframework.data.domain.Pageable;
import ru.ulstu.is.server.dto.PageHelper;
import ru.ulstu.is.server.dto.PageRs;
import ru.ulstu.is.server.entity.Movie;
import ru.ulstu.is.server.mapper.MovieMapper;
import ru.ulstu.is.server.repository.DirectorRepository;
@@ -42,10 +45,8 @@ public class MovieService {
}
@Transactional(readOnly = true)
public List<MovieRs> getPage(int page, int size) {
return movies.findAll(PageRequest.of(page, size))
.map(mapper::toRs)
.toList();
public PageRs<MovieRs> getPage(Pageable pageable) {
return PageRs.from(movies.findAll(pageable), mapper::toRs);
}
@Transactional(readOnly = true)

View File

@@ -0,0 +1,19 @@
spring:
datasource:
url: jdbc:h2:file:./data/appdb
username: sa
password:
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.springdoc: DEBUG

View File

@@ -0,0 +1,4 @@
spring:
web:
resources:
add-mappings: true

View File

@@ -0,0 +1,9 @@
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:5432/internet_lab4
username: postgres
password: 1234
driver-class-name: org.postgresql.Driver
jpa:
show-sql: false

View File

@@ -1,18 +0,0 @@
spring.main.banner-mode=off
spring.application.name=server
server.port=8080
spring.datasource.url=jdbc:h2:file:./data/appdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2
# logging
logging.level.ru.ulstu.is.server=DEBUG

View File

@@ -0,0 +1,16 @@
spring:
liquibase:
change-log: classpath:db/changelog/db.changelog-master.yaml
application:
name: server
main:
banner-mode: off
profiles:
active: dev
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
server:
port: 8080

View File

@@ -0,0 +1,26 @@
databaseChangeLog:
- changeSet:
id: add-review-table
author: busla
changes:
- createTable:
tableName: reviews
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: movie_id
type: BIGINT
constraints:
nullable: false
- column:
name: text
type: VARCHAR(1000)
- column:
name: rating
type: INT

View File

@@ -0,0 +1,334 @@
databaseChangeLog:
- changeSet:
id: 1763112391251-1
author: busla (generated)
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
nullable: false
primaryKey: true
primaryKeyName: CONSTRAINT_6
name: ID
type: BIGINT
- column:
constraints:
nullable: false
name: NAME
type: VARCHAR(255)
tableName: DIRECTORS
- changeSet:
id: 1763112391251-2
author: busla (generated)
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
nullable: false
primaryKey: true
primaryKeyName: CONSTRAINT_7
name: ID
type: BIGINT
- column:
constraints:
nullable: false
name: NAME
type: VARCHAR(255)
tableName: GENRES
- changeSet:
id: 1763112391251-3
author: busla (generated)
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
nullable: false
primaryKey: true
primaryKeyName: CONSTRAINT_8
name: ID
type: BIGINT
- column:
constraints:
nullable: false
name: GRADE
type: REAL
- column:
name: IMAGE
type: CHARACTER LARGE OBJECT
- column:
constraints:
nullable: false
name: TITLE
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: DIRECTOR_ID
type: BIGINT
- column:
constraints:
nullable: false
name: GENRE_ID
type: BIGINT
tableName: MOVIES
- changeSet:
id: 1763112391251-4
author: busla (generated)
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
nullable: false
primaryKey: true
primaryKeyName: CONSTRAINT_3
name: ID
type: BIGINT
- column:
constraints:
nullable: false
name: LEVEL
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: PRICE
type: INT
tableName: SUBSCRIPTIONS
- changeSet:
id: 1763112391251-5
author: busla (generated)
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
nullable: false
primaryKey: true
primaryKeyName: CONSTRAINT_4
name: ID
type: BIGINT
- column:
name: AGE
type: INT
- column:
name: AVATAR
type: CHARACTER LARGE OBJECT
- column:
name: GENDER
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: IS_ADMIN
type: BOOLEAN
- column:
constraints:
nullable: false
name: LOGIN
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: NAME
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: PASSWORD
type: VARCHAR(255)
- column:
name: SUBSCRIPTION_UNTIL
type: date
- column:
name: SUBSCRIPTION_ID
type: BIGINT
tableName: USERS
- changeSet:
id: 1763112391251-6
author: busla (generated)
changes:
- createTable:
columns:
- column:
constraints:
nullable: false
name: USER_ID
type: BIGINT
- column:
constraints:
nullable: false
name: MOVIE_ID
type: BIGINT
tableName: USER_WATCHLIST
- changeSet:
id: 1763112391251-7
author: busla (generated)
changes:
- addUniqueConstraint:
columnNames: LEVEL
constraintName: UKAVRYNEIAOOG6H0RWOC9QAHFGU
tableName: SUBSCRIPTIONS
- changeSet:
id: 1763112391251-8
author: busla (generated)
changes:
- addUniqueConstraint:
columnNames: LOGIN
constraintName: UKOW0GAN20590JRB00UPG3VA2FN
tableName: USERS
- changeSet:
id: 1763112391251-9
author: busla (generated)
changes:
- addUniqueConstraint:
columnNames: NAME
constraintName: UKPE1A9WOIK1K97L87CIEGUYHH4
tableName: GENRES
- changeSet:
id: 1763112391251-10
author: busla (generated)
changes:
- addUniqueConstraint:
columnNames: NAME
constraintName: UKT6U48CMKTDMYIEUC2B6Y4UMN
tableName: DIRECTORS
- changeSet:
id: 1763112391251-11
author: busla (generated)
changes:
- createIndex:
associatedWith: ''
columns:
- column:
name: USER_ID
indexName: FK1A2SF5HA20F8A3SQGO3N4H3W6_INDEX_5
tableName: USER_WATCHLIST
- changeSet:
id: 1763112391251-12
author: busla (generated)
changes:
- createIndex:
associatedWith: ''
columns:
- column:
name: DIRECTOR_ID
indexName: FK5FT3U8K962BMJD8RN2MR77J8D_INDEX_8
tableName: MOVIES
- changeSet:
id: 1763112391251-13
author: busla (generated)
changes:
- createIndex:
associatedWith: ''
columns:
- column:
name: MOVIE_ID
indexName: FK9FHBUF2RH3U2D0CITGJX86ID9_INDEX_5
tableName: USER_WATCHLIST
- changeSet:
id: 1763112391251-14
author: busla (generated)
changes:
- createIndex:
associatedWith: ''
columns:
- column:
name: SUBSCRIPTION_ID
indexName: FKFWX079XWW5UYFBPI9U8GWAM34_INDEX_4
tableName: USERS
- changeSet:
id: 1763112391251-15
author: busla (generated)
changes:
- createIndex:
associatedWith: ''
columns:
- column:
name: GENRE_ID
indexName: FKJP8FSY8A0KKMDI04I81V05C6A_INDEX_8
tableName: MOVIES
- changeSet:
id: 1763112391251-16
author: busla (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: USER_ID
baseTableName: USER_WATCHLIST
constraintName: FK1A2SF5HA20F8A3SQGO3N4H3W6
deferrable: false
initiallyDeferred: false
onDelete: RESTRICT
onUpdate: RESTRICT
referencedColumnNames: ID
referencedTableName: USERS
validate: true
- changeSet:
id: 1763112391251-17
author: busla (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: DIRECTOR_ID
baseTableName: MOVIES
constraintName: FK5FT3U8K962BMJD8RN2MR77J8D
deferrable: false
initiallyDeferred: false
onDelete: RESTRICT
onUpdate: RESTRICT
referencedColumnNames: ID
referencedTableName: DIRECTORS
validate: true
- changeSet:
id: 1763112391251-18
author: busla (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: MOVIE_ID
baseTableName: USER_WATCHLIST
constraintName: FK9FHBUF2RH3U2D0CITGJX86ID9
deferrable: false
initiallyDeferred: false
onDelete: RESTRICT
onUpdate: RESTRICT
referencedColumnNames: ID
referencedTableName: MOVIES
validate: true
- changeSet:
id: 1763112391251-19
author: busla (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: SUBSCRIPTION_ID
baseTableName: USERS
constraintName: FKFWX079XWW5UYFBPI9U8GWAM34
deferrable: false
initiallyDeferred: false
onDelete: RESTRICT
onUpdate: RESTRICT
referencedColumnNames: ID
referencedTableName: SUBSCRIPTIONS
validate: true
- changeSet:
id: 1763112391251-20
author: busla (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: GENRE_ID
baseTableName: MOVIES
constraintName: FKJP8FSY8A0KKMDI04I81V05C6A
deferrable: false
initiallyDeferred: false
onDelete: RESTRICT
onUpdate: RESTRICT
referencedColumnNames: ID
referencedTableName: GENRES
validate: true

View File

@@ -0,0 +1,7 @@
databaseChangeLog:
- include:
file: db.changelog-001-initial.yaml
relativeToChangelogFile: true
- include:
file: changeset-3-add-review.yaml
relativeToChangelogFile: true

View File

@@ -1,8 +0,0 @@
logging.level.ru.ulstu.is.server=DEBUG
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false

View File

@@ -0,0 +1,10 @@
spring:
profiles:
active: dev
jpa:
hibernate:
ddl-auto: create-drop
liquibase:
enabled: false

20303
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,40 @@
import { useEffect, useState } from "react";
import axios from "axios";
export default function useMovies() {
const [movies, setMovies] = useState([]);
export default function useMovies(initialPage = 1, pageSize = 12) {
const [page, setPage] = useState(initialPage);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [reload, setReload] = useState(0);
useEffect(() => {
axios.get("http://localhost:8080/api/1.0/movies")
.then(res => setMovies(res.data))
.catch(setError)
setLoading(true);
axios
.get("http://localhost:8080/api/1.0/movies/page", {
params: { page, size: pageSize },
})
.then((res) => setData(res.data))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, []);
}, [page, pageSize, reload]);
const remove = async (id) => {
try {
await axios.delete(`http://localhost:8080/api/1.0/movies/${id}`);
setMovies(prev => prev.filter(m => m.id !== id));
setReload((r) => r + 1);
} catch (err) {
console.error("Ошибка при удалении:", err);
}
};
return { movies, loading, error, remove };
return {
pageData: data,
loading,
error,
page,
setPage,
pageSize,
remove,
};
}

View File

@@ -1,12 +1,56 @@
import useMovies from "../hooks/useMovies";
import MovieCard from "../components/MovieCard";
import { Link } from "react-router-dom";
import { Link } from "react-router-dom";
export default function Movies() {
const { movies, loading, error, remove } = useMovies();
const {
pageData,
loading,
error,
page,
setPage,
remove,
} = useMovies(1, 12);
if (loading) return <p className="text-center py-5">Загрузка</p>;
if (error) return <p className="text-danger py-5 text-center">Ошибка: {error.message}</p>;
if (loading) {
return <p className="text-center py-5">Загрузка</p>;
}
if (error) {
return (
<p className="text-danger py-5 text-center">
Ошибка: {error.message}
</p>
);
}
if (!pageData || pageData.items.length === 0) {
return (
<div className="container py-4">
<div className="d-flex justify-content-between align-items-center mb-4">
<h1 className="h3 m-0">Каталог фильмов</h1>
<Link to="/new" className="btn btn-success">
+ Добавить
</Link>
</div>
<p>Фильмы не найдены.</p>
</div>
);
}
const { items, totalPages, hasNext, hasPrevious } = pageData;
const goPrev = () => {
if (hasPrevious && page > 1) {
setPage(page - 1);
}
};
const goNext = () => {
if (hasNext && page < totalPages) {
setPage(page + 1);
}
};
return (
<div className="container py-4">
@@ -18,13 +62,36 @@ export default function Movies() {
</Link>
</div>
<div className="row g-4">
{movies.map((m) => (
<div className="row g-4 mb-4">
{items.map((m) => (
<div className="col-6 col-md-4 col-lg-3" key={m.id}>
<MovieCard movie={m} onDelete={remove} />
<MovieCard movie={m} onDelete={() => remove(m.id)} />
</div>
))}
</div>
{}
<div className="d-flex justify-content-center align-items-center gap-3">
<button
className="btn btn-outline-secondary"
onClick={goPrev}
disabled={!hasPrevious}
>
Назад
</button>
<span>
Страница {page} из {totalPages}
</span>
<button
className="btn btn-outline-secondary"
onClick={goNext}
disabled={!hasNext}
>
Вперёд
</button>
</div>
</div>
);
}

View File

@@ -1,17 +1,11 @@
// @ts-ignore
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/movies': 'http://localhost:3000',
'/genres': 'http://localhost:3000',
'/directors': 'http://localhost:3000',
'/users': 'http://localhost:3000',
'/subscriptions': 'http://localhost:3000',
'/purchases': 'http://localhost:3000',
},
build: {
sourcemap: true,
emptyOutDir: true,
outDir: "../build/resources/main/static",
},
});

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "ипРома",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}