This commit is contained in:
王子琦
2026-01-13 17:32:13 +08:00
parent 8572733b2e
commit f567e733d3
11 changed files with 1301 additions and 4 deletions

32
.gitignore vendored Normal file
View 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~

View File

@@ -2,6 +2,7 @@ package com.toyshop.config;
import com.toyshop.dto.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -23,4 +24,10 @@ public class RestExceptionHandler {
: ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return ApiResponse.fail(msg);
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ApiResponse<?> handleAuth(AuthenticationException ex) {
return ApiResponse.fail("用户名或密码错误");
}
}

View File

@@ -7,6 +7,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -37,7 +38,8 @@ public class SecurityConfig {
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/auth/**", "/api/public/**", "/error").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@@ -60,9 +62,10 @@ public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("*"));
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;

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

View File

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

View File

@@ -1,8 +1,10 @@
package com.toyshop.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
@Entity
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@Table(name = "categories")
public class Category {
@Id

View File

@@ -1,5 +1,6 @@
package com.toyshop.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -11,8 +12,9 @@ public class Product {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "category_id")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Category category;
@Column(nullable = false, length = 100)

View File

@@ -18,3 +18,5 @@ app:
jwt:
secret: change-this-secret-for-prod-change-this-secret
expire-hours: 24
upload:
dir: D:/bs/shopping/files

1161
frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,11 @@
<template>
<a-card title="轮播图管理">
<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="链接"><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>
@@ -22,6 +27,8 @@ import api from '../../api'
const list = ref([])
const form = reactive({ imageUrl: '', linkUrl: '', sortOrder: 0 })
const uploadUrl = 'http://localhost:8080/api/admin/upload'
const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` }
const columns = [
{ title: '图片', dataIndex: 'imageUrl' },
{ title: '链接', dataIndex: 'linkUrl' },
@@ -42,6 +49,15 @@ const create = async () => {
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) => {
await api.delete(`/api/admin/carousels/${record.id}`)
load()

View File

@@ -20,7 +20,10 @@
</template>
</a-table>
<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-modal>
</a-card>
@@ -42,6 +45,8 @@ const columns = [
const imgVisible = ref(false)
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 res = await api.get('/api/admin/products')
@@ -76,5 +81,14 @@ const saveImage = async () => {
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)
</script>