add
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Java
|
||||||
|
backend/target/
|
||||||
|
backend/.mvn/
|
||||||
|
backend/.idea/
|
||||||
|
backend/*.iml
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Database dumps
|
||||||
|
*.sql~
|
||||||
@@ -2,6 +2,7 @@ package com.toyshop.config;
|
|||||||
|
|
||||||
import com.toyshop.dto.ApiResponse;
|
import com.toyshop.dto.ApiResponse;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
@@ -23,4 +24,10 @@ public class RestExceptionHandler {
|
|||||||
: ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
|
: ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
|
||||||
return ApiResponse.fail(msg);
|
return ApiResponse.fail(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
|
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||||
|
public ApiResponse<?> handleAuth(AuthenticationException ex) {
|
||||||
|
return ApiResponse.fail("用户名或密码错误");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.security.authentication.AuthenticationManager;
|
|||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
@@ -37,7 +38,8 @@ public class SecurityConfig {
|
|||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
|
.requestMatchers("/api/auth/**", "/api/public/**", "/error").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
@@ -60,9 +62,10 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration config = new CorsConfiguration();
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
config.setAllowedOrigins(List.of("*"));
|
config.setAllowedOriginPatterns(List.of("*"));
|
||||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
config.setAllowedHeaders(List.of("*"));
|
config.setAllowedHeaders(List.of("*"));
|
||||||
|
config.setAllowCredentials(false);
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
source.registerCorsConfiguration("/**", config);
|
source.registerCorsConfiguration("/**", config);
|
||||||
return source;
|
return source;
|
||||||
|
|||||||
18
backend/src/main/java/com/toyshop/config/WebConfig.java
Normal file
18
backend/src/main/java/com/toyshop/config/WebConfig.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.toyshop.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
@Value("${app.upload.dir}")
|
||||||
|
private String uploadDir;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
String location = "file:/" + uploadDir.replace("\\", "/") + "/";
|
||||||
|
registry.addResourceHandler("/files/**").addResourceLocations(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.toyshop.controller.admin;
|
||||||
|
|
||||||
|
import com.toyshop.dto.ApiResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public class UploadController {
|
||||||
|
@Value("${app.upload.dir}")
|
||||||
|
private String uploadDir;
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public ApiResponse<?> upload(@RequestParam("file") MultipartFile file) throws IOException {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return ApiResponse.fail("文件为空");
|
||||||
|
}
|
||||||
|
String original = file.getOriginalFilename();
|
||||||
|
String ext = StringUtils.getFilenameExtension(original);
|
||||||
|
String filename = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
if (ext != null && !ext.isBlank()) {
|
||||||
|
filename = filename + "." + ext;
|
||||||
|
}
|
||||||
|
Path target = Paths.get(uploadDir, filename);
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
file.transferTo(target);
|
||||||
|
String url = "/files/" + filename;
|
||||||
|
return ApiResponse.ok(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.toyshop.entity;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
|
||||||
@Table(name = "categories")
|
@Table(name = "categories")
|
||||||
public class Category {
|
public class Category {
|
||||||
@Id
|
@Id
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.toyshop.entity;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -11,8 +12,9 @@ public class Product {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "category_id")
|
@JoinColumn(name = "category_id")
|
||||||
|
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
|
||||||
private Category category;
|
private Category category;
|
||||||
|
|
||||||
@Column(nullable = false, length = 100)
|
@Column(nullable = false, length = 100)
|
||||||
|
|||||||
@@ -18,3 +18,5 @@ app:
|
|||||||
jwt:
|
jwt:
|
||||||
secret: change-this-secret-for-prod-change-this-secret
|
secret: change-this-secret-for-prod-change-this-secret
|
||||||
expire-hours: 24
|
expire-hours: 24
|
||||||
|
upload:
|
||||||
|
dir: D:/bs/shopping/files
|
||||||
|
|||||||
1161
frontend/pnpm-lock.yaml
generated
Normal file
1161
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-card title="轮播图管理">
|
<a-card title="轮播图管理">
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
|
<a-form-item label="图片">
|
||||||
|
<a-upload :action="uploadUrl" :headers="headers" @change="onUpload" :showUploadList="false">
|
||||||
|
<a-button>上传图片</a-button>
|
||||||
|
</a-upload>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="图片URL"><a-input v-model:value="form.imageUrl" /></a-form-item>
|
<a-form-item label="图片URL"><a-input v-model:value="form.imageUrl" /></a-form-item>
|
||||||
<a-form-item label="链接"><a-input v-model:value="form.linkUrl" /></a-form-item>
|
<a-form-item label="链接"><a-input v-model:value="form.linkUrl" /></a-form-item>
|
||||||
<a-form-item label="排序"><a-input-number v-model:value="form.sortOrder" /></a-form-item>
|
<a-form-item label="排序"><a-input-number v-model:value="form.sortOrder" /></a-form-item>
|
||||||
@@ -22,6 +27,8 @@ import api from '../../api'
|
|||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const form = reactive({ imageUrl: '', linkUrl: '', sortOrder: 0 })
|
const form = reactive({ imageUrl: '', linkUrl: '', sortOrder: 0 })
|
||||||
|
const uploadUrl = 'http://localhost:8080/api/admin/upload'
|
||||||
|
const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` }
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '图片', dataIndex: 'imageUrl' },
|
{ title: '图片', dataIndex: 'imageUrl' },
|
||||||
{ title: '链接', dataIndex: 'linkUrl' },
|
{ title: '链接', dataIndex: 'linkUrl' },
|
||||||
@@ -42,6 +49,15 @@ const create = async () => {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onUpload = (info) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
const res = info.file.response
|
||||||
|
if (res && res.success) {
|
||||||
|
form.imageUrl = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const remove = async (record) => {
|
const remove = async (record) => {
|
||||||
await api.delete(`/api/admin/carousels/${record.id}`)
|
await api.delete(`/api/admin/carousels/${record.id}`)
|
||||||
load()
|
load()
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
<a-modal v-model:open="imgVisible" title="新增图片" @ok="saveImage">
|
<a-modal v-model:open="imgVisible" title="新增图片" @ok="saveImage">
|
||||||
<a-input v-model:value="imgForm.url" placeholder="图片URL" />
|
<a-upload :action="uploadUrl" :headers="headers" @change="onUpload" :showUploadList="false">
|
||||||
|
<a-button>上传图片</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<a-input v-model:value="imgForm.url" placeholder="图片URL" style="margin-top: 8px" />
|
||||||
<a-input-number v-model:value="imgForm.sortOrder" style="margin-top: 8px" />
|
<a-input-number v-model:value="imgForm.sortOrder" style="margin-top: 8px" />
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -42,6 +45,8 @@ const columns = [
|
|||||||
|
|
||||||
const imgVisible = ref(false)
|
const imgVisible = ref(false)
|
||||||
const imgForm = reactive({ productId: null, url: '', sortOrder: 0 })
|
const imgForm = reactive({ productId: null, url: '', sortOrder: 0 })
|
||||||
|
const uploadUrl = 'http://localhost:8080/api/admin/upload'
|
||||||
|
const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` }
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const res = await api.get('/api/admin/products')
|
const res = await api.get('/api/admin/products')
|
||||||
@@ -76,5 +81,14 @@ const saveImage = async () => {
|
|||||||
imgVisible.value = false
|
imgVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onUpload = (info) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
const res = info.file.response
|
||||||
|
if (res && res.success) {
|
||||||
|
imgForm.url = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user