diff --git a/backend/src/main/java/com/toyshop/config/SecurityConfig.java b/backend/src/main/java/com/toyshop/config/SecurityConfig.java index 558ed46..2a855ea 100644 --- a/backend/src/main/java/com/toyshop/config/SecurityConfig.java +++ b/backend/src/main/java/com/toyshop/config/SecurityConfig.java @@ -39,7 +39,7 @@ public class SecurityConfig { .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .requestMatchers("/api/auth/**", "/api/public/**", "/error").permitAll() + .requestMatchers("/api/auth/**", "/api/public/**", "/files/**", "/error").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/backend/src/main/java/com/toyshop/config/WebConfig.java b/backend/src/main/java/com/toyshop/config/WebConfig.java index f7cf4b1..bc81f46 100644 --- a/backend/src/main/java/com/toyshop/config/WebConfig.java +++ b/backend/src/main/java/com/toyshop/config/WebConfig.java @@ -1,4 +1,4 @@ -package com.toyshop.config; +package com.toyshop.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/com/toyshop/controller/admin/AdminController.java b/backend/src/main/java/com/toyshop/controller/admin/AdminController.java index 43939eb..5f69dc2 100644 --- a/backend/src/main/java/com/toyshop/controller/admin/AdminController.java +++ b/backend/src/main/java/com/toyshop/controller/admin/AdminController.java @@ -81,7 +81,14 @@ public class AdminController { @GetMapping("/products") public ApiResponse> products() { - return ApiResponse.ok(productRepository.findAll()); + List products = productRepository.findAll(); + for (Product product : products) { + var img = productService.getFirstImage(product); + if (img != null) { + product.setImageUrl(img.getUrl()); + } + } + return ApiResponse.ok(products); } @PostMapping("/products") @@ -148,6 +155,16 @@ public class AdminController { return ApiResponse.ok("删除成功", null); } + @PutMapping("/products/{id}/image") + public ApiResponse replaceProductImage(@PathVariable Long id, @RequestBody ProductImage body) { + Product product = productRepository.findById(id).orElseThrow(); + if (body.getUrl() == null || body.getUrl().isBlank()) { + return ApiResponse.fail("图片地址不能为空"); + } + productService.replaceProductImage(product, body.getUrl()); + return ApiResponse.ok("更新成功", null); + } + @GetMapping("/orders") public ApiResponse> orders() { return ApiResponse.ok(orderRepository.findAll()); diff --git a/backend/src/main/java/com/toyshop/controller/admin/UploadController.java b/backend/src/main/java/com/toyshop/controller/admin/UploadController.java index 6447b74..015adb0 100644 --- a/backend/src/main/java/com/toyshop/controller/admin/UploadController.java +++ b/backend/src/main/java/com/toyshop/controller/admin/UploadController.java @@ -1,4 +1,4 @@ -package com.toyshop.controller.admin; +package com.toyshop.controller.admin; import com.toyshop.dto.ApiResponse; import org.springframework.beans.factory.annotation.Value; diff --git a/backend/src/main/java/com/toyshop/entity/Address.java b/backend/src/main/java/com/toyshop/entity/Address.java index c47a73b..2d2e646 100644 --- a/backend/src/main/java/com/toyshop/entity/Address.java +++ b/backend/src/main/java/com/toyshop/entity/Address.java @@ -1,5 +1,6 @@ package com.toyshop.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -12,6 +13,7 @@ public class Address { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore private User user; @Column(nullable = false, length = 50) diff --git a/backend/src/main/java/com/toyshop/entity/CartItem.java b/backend/src/main/java/com/toyshop/entity/CartItem.java index 4ed23a1..4a1f98d 100644 --- a/backend/src/main/java/com/toyshop/entity/CartItem.java +++ b/backend/src/main/java/com/toyshop/entity/CartItem.java @@ -1,5 +1,7 @@ package com.toyshop.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; @Entity @@ -11,10 +13,12 @@ public class CartItem { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore private User user; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "product_id", nullable = false) + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) private Product product; @Column(nullable = false) diff --git a/backend/src/main/java/com/toyshop/entity/Order.java b/backend/src/main/java/com/toyshop/entity/Order.java index 6092527..a74cad5 100644 --- a/backend/src/main/java/com/toyshop/entity/Order.java +++ b/backend/src/main/java/com/toyshop/entity/Order.java @@ -1,5 +1,6 @@ package com.toyshop.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -16,6 +17,7 @@ public class Order { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore private User user; @Enumerated(EnumType.STRING) diff --git a/backend/src/main/java/com/toyshop/entity/OrderItem.java b/backend/src/main/java/com/toyshop/entity/OrderItem.java index 9c70718..29ebc55 100644 --- a/backend/src/main/java/com/toyshop/entity/OrderItem.java +++ b/backend/src/main/java/com/toyshop/entity/OrderItem.java @@ -1,5 +1,7 @@ package com.toyshop.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import java.math.BigDecimal; @@ -12,10 +14,12 @@ public class OrderItem { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id", nullable = false) + @JsonIgnore private Order order; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "product_id", nullable = false) + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) private Product product; @Column(nullable = false) diff --git a/backend/src/main/java/com/toyshop/entity/Product.java b/backend/src/main/java/com/toyshop/entity/Product.java index 2c7dc31..3f5a06b 100644 --- a/backend/src/main/java/com/toyshop/entity/Product.java +++ b/backend/src/main/java/com/toyshop/entity/Product.java @@ -47,6 +47,9 @@ public class Product { @Column(nullable = false) private LocalDateTime updatedAt; + @Transient + private String imageUrl; + @PrePersist public void prePersist() { LocalDateTime now = LocalDateTime.now(); @@ -81,4 +84,6 @@ public class Product { public void setOnSale(boolean onSale) { this.onSale = onSale; } public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } + public String getImageUrl() { return imageUrl; } + public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } } diff --git a/backend/src/main/java/com/toyshop/entity/ProductImage.java b/backend/src/main/java/com/toyshop/entity/ProductImage.java index 21a9e2d..8ab0209 100644 --- a/backend/src/main/java/com/toyshop/entity/ProductImage.java +++ b/backend/src/main/java/com/toyshop/entity/ProductImage.java @@ -1,5 +1,6 @@ package com.toyshop.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; @Entity @@ -11,6 +12,7 @@ public class ProductImage { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) + @JsonIgnore private Product product; @Column(nullable = false, length = 255) diff --git a/backend/src/main/java/com/toyshop/repository/ProductImageRepository.java b/backend/src/main/java/com/toyshop/repository/ProductImageRepository.java index ba52dc5..3cfdd96 100644 --- a/backend/src/main/java/com/toyshop/repository/ProductImageRepository.java +++ b/backend/src/main/java/com/toyshop/repository/ProductImageRepository.java @@ -4,7 +4,10 @@ import com.toyshop.entity.ProductImage; import com.toyshop.entity.Product; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface ProductImageRepository extends JpaRepository { List findByProduct(Product product); + Optional findTopByProductOrderBySortOrderAsc(Product product); + void deleteByProduct(Product product); } diff --git a/backend/src/main/java/com/toyshop/service/OrderService.java b/backend/src/main/java/com/toyshop/service/OrderService.java index af38594..b7b4863 100644 --- a/backend/src/main/java/com/toyshop/service/OrderService.java +++ b/backend/src/main/java/com/toyshop/service/OrderService.java @@ -3,6 +3,7 @@ package com.toyshop.service; import com.toyshop.entity.*; import com.toyshop.repository.*; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.List; @@ -23,6 +24,7 @@ public class OrderService { this.addressRepository = addressRepository; } + @Transactional public Order createOrder(User user, Long addressId) { Address address = addressRepository.findById(addressId).orElseThrow(); if (!address.getUser().getId().equals(user.getId())) { diff --git a/backend/src/main/java/com/toyshop/service/ProductService.java b/backend/src/main/java/com/toyshop/service/ProductService.java index 7119cad..df8a6e3 100644 --- a/backend/src/main/java/com/toyshop/service/ProductService.java +++ b/backend/src/main/java/com/toyshop/service/ProductService.java @@ -5,6 +5,7 @@ import com.toyshop.repository.*; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.criteria.Predicate; import java.util.ArrayList; @@ -59,4 +60,18 @@ public class ProductService { public List allCategories() { return categoryRepository.findAll(); } + + public ProductImage getFirstImage(Product product) { + return productImageRepository.findTopByProductOrderBySortOrderAsc(product).orElse(null); + } + + @Transactional + public void replaceProductImage(Product product, String url) { + productImageRepository.deleteByProduct(product); + ProductImage image = new ProductImage(); + image.setProduct(product); + image.setUrl(url); + image.setSortOrder(0); + productImageRepository.save(image); + } } diff --git a/files/14771fb97130455d9b3108f8ab91a0b8.jpg b/files/14771fb97130455d9b3108f8ab91a0b8.jpg new file mode 100644 index 0000000..bb37a07 Binary files /dev/null and b/files/14771fb97130455d9b3108f8ab91a0b8.jpg differ diff --git a/files/172ef1e1cac649b8a3e801b8a0accbb5.png b/files/172ef1e1cac649b8a3e801b8a0accbb5.png new file mode 100644 index 0000000..2a422f6 Binary files /dev/null and b/files/172ef1e1cac649b8a3e801b8a0accbb5.png differ diff --git a/files/4aecfad04b8a442b951bb31f5a917ba2.jpg b/files/4aecfad04b8a442b951bb31f5a917ba2.jpg new file mode 100644 index 0000000..40c97ba Binary files /dev/null and b/files/4aecfad04b8a442b951bb31f5a917ba2.jpg differ diff --git a/files/814b34b4b50146f2b2f6981120e14917.png b/files/814b34b4b50146f2b2f6981120e14917.png new file mode 100644 index 0000000..5d602f9 Binary files /dev/null and b/files/814b34b4b50146f2b2f6981120e14917.png differ diff --git a/files/8388273805a141b391faf586e549f853.png b/files/8388273805a141b391faf586e549f853.png new file mode 100644 index 0000000..611483c Binary files /dev/null and b/files/8388273805a141b391faf586e549f853.png differ diff --git a/files/9f81853ab69745c4b4f59d6b6897e96c.png b/files/9f81853ab69745c4b4f59d6b6897e96c.png new file mode 100644 index 0000000..611483c Binary files /dev/null and b/files/9f81853ab69745c4b4f59d6b6897e96c.png differ diff --git a/files/ab2cef0445854980b06548b5f7114b6a.jpg b/files/ab2cef0445854980b06548b5f7114b6a.jpg new file mode 100644 index 0000000..145eef1 Binary files /dev/null and b/files/ab2cef0445854980b06548b5f7114b6a.jpg differ diff --git a/files/d180c0ef0ded4b3fac45717b531a9cb0.jpg b/files/d180c0ef0ded4b3fac45717b531a9cb0.jpg new file mode 100644 index 0000000..145eef1 Binary files /dev/null and b/files/d180c0ef0ded4b3fac45717b531a9cb0.jpg differ diff --git a/files/e30bf07aa40747e38ae54de8eb3ad7f1.jpg b/files/e30bf07aa40747e38ae54de8eb3ad7f1.jpg new file mode 100644 index 0000000..bb37a07 Binary files /dev/null and b/files/e30bf07aa40747e38ae54de8eb3ad7f1.jpg differ diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index c20cd3d..1e1eb72 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,4 +1,5 @@ import axios from 'axios' +import router from '../router' const api = axios.create({ baseURL: 'http://localhost:8080', @@ -16,6 +17,14 @@ api.interceptors.request.use((config) => { api.interceptors.response.use( (res) => res.data, (err) => { + const status = err?.response?.status + if (status === 401 || status === 403) { + const role = localStorage.getItem('role') + const target = role === 'ADMIN' ? '/admin/login' : '/login' + if (router.currentRoute.value.path !== target) { + router.push(target) + } + } const message = err?.response?.data?.message || '请求失败' return Promise.reject(new Error(message)) } diff --git a/frontend/src/pages/admin/AdminCarousels.vue b/frontend/src/pages/admin/AdminCarousels.vue index 9bd33a5..328e45d 100644 --- a/frontend/src/pages/admin/AdminCarousels.vue +++ b/frontend/src/pages/admin/AdminCarousels.vue @@ -13,11 +13,15 @@ + @@ -27,10 +31,11 @@ import api from '../../api' const list = ref([]) const form = reactive({ imageUrl: '', linkUrl: '', sortOrder: 0 }) -const uploadUrl = 'http://localhost:8080/api/admin/upload' +const baseUrl = 'http://localhost:8080' +const uploadUrl = `${baseUrl}/api/admin/upload` const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` } const columns = [ - { title: '图片', dataIndex: 'imageUrl' }, + { title: '图片', key: 'image' }, { title: '链接', dataIndex: 'linkUrl' }, { title: '排序', dataIndex: 'sortOrder' }, { title: '操作', key: 'action' } @@ -53,11 +58,19 @@ const onUpload = (info) => { if (info.file.status === 'done') { const res = info.file.response if (res && res.success) { - form.imageUrl = res.data + form.imageUrl = normalizeUrl(res.data) } } } +const normalizeUrl = (url) => { + if (!url) return '' + if (url.startsWith('http')) return url + return `${baseUrl}${url}` +} + +const previewUrl = (url) => normalizeUrl(url) + const remove = async (record) => { await api.delete(`/api/admin/carousels/${record.id}`) load() @@ -65,3 +78,18 @@ const remove = async (record) => { onMounted(load) + + diff --git a/frontend/src/pages/admin/AdminProducts.vue b/frontend/src/pages/admin/AdminProducts.vue index 7751042..ae01045 100644 --- a/frontend/src/pages/admin/AdminProducts.vue +++ b/frontend/src/pages/admin/AdminProducts.vue @@ -13,18 +13,41 @@ - + 上传图片 - - + + + + + + + + + + + {{ c.name }} + + + + + + + + + @@ -40,13 +63,27 @@ const columns = [ { title: '名称', dataIndex: 'name' }, { title: '价格', dataIndex: 'price' }, { title: '库存', dataIndex: 'stock' }, + { title: '图片', key: 'image' }, { title: '操作', key: 'action' } ] const imgVisible = ref(false) -const imgForm = reactive({ productId: null, url: '', sortOrder: 0 }) -const uploadUrl = 'http://localhost:8080/api/admin/upload' +const imgForm = reactive({ productId: null, url: '' }) +const baseUrl = 'http://localhost:8080' +const uploadUrl = `${baseUrl}/api/admin/upload` const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` } +const editVisible = ref(false) +const editForm = reactive({ + id: null, + name: '', + price: 0, + stock: 0, + categoryId: null, + ageRange: '', + safetyInfo: '', + description: '', + onSale: true +}) const load = async () => { const res = await api.get('/api/admin/products') @@ -71,24 +108,67 @@ const remove = async (record) => { const addImage = (record) => { imgForm.productId = record.id - imgForm.url = '' - imgForm.sortOrder = 0 + imgForm.url = record.imageUrl || '' imgVisible.value = true } const saveImage = async () => { - await api.post(`/api/admin/products/${imgForm.productId}/images`, imgForm) + await api.put(`/api/admin/products/${imgForm.productId}/image`, { url: imgForm.url }) imgVisible.value = false + load() } const onUpload = (info) => { if (info.file.status === 'done') { const res = info.file.response if (res && res.success) { - imgForm.url = res.data + imgForm.url = normalizeUrl(res.data) } } } +const normalizeUrl = (url) => { + if (!url) return '' + if (url.startsWith('http')) return url + return `${baseUrl}${url}` +} + +const previewUrl = (url) => normalizeUrl(url) + +const edit = (record) => { + editForm.id = record.id + editForm.name = record.name + editForm.price = record.price + editForm.stock = record.stock + editForm.categoryId = record.category?.id || null + editForm.ageRange = record.ageRange || '' + editForm.safetyInfo = record.safetyInfo || '' + editForm.description = record.description || '' + editForm.onSale = record.onSale + editVisible.value = true +} + +const saveEdit = async () => { + await api.put(`/api/admin/products/${editForm.id}`, editForm) + editVisible.value = false + load() +} + onMounted(load) + + diff --git a/frontend/src/pages/user/Checkout.vue b/frontend/src/pages/user/Checkout.vue index 121bf93..0408dfa 100644 --- a/frontend/src/pages/user/Checkout.vue +++ b/frontend/src/pages/user/Checkout.vue @@ -1,14 +1,16 @@ diff --git a/frontend/src/pages/user/Home.vue b/frontend/src/pages/user/Home.vue index f505a69..f0c76d2 100644 --- a/frontend/src/pages/user/Home.vue +++ b/frontend/src/pages/user/Home.vue @@ -3,7 +3,7 @@ @@ -46,6 +46,7 @@ const loading = ref(false) const home = reactive({ carousels: [], notices: [], hot: [], newest: [] }) const router = useRouter() +const baseUrl = 'http://localhost:8080' const load = async () => { loading.value = true try { @@ -58,6 +59,12 @@ const load = async () => { const goDetail = (id) => router.push(`/products/${id}`) +const previewUrl = (url) => { + if (!url) return '' + if (url.startsWith('http')) return url + return `${baseUrl}${url}` +} + onMounted(load) diff --git a/frontend/src/pages/user/Orders.vue b/frontend/src/pages/user/Orders.vue index 3050283..7d57fe6 100644 --- a/frontend/src/pages/user/Orders.vue +++ b/frontend/src/pages/user/Orders.vue @@ -6,11 +6,38 @@ {{ statusMap[record.status] || record.status }} + + + + {{ detail.order?.orderNo }} + ¥{{ detail.order?.totalAmount }} + {{ statusMap[detail.order?.status] }} + {{ detail.order?.receiverName }} + {{ detail.order?.receiverPhone }} + {{ detail.order?.receiverAddress }} + {{ detail.order?.logisticsNo || '-' }} + + + + + + diff --git a/frontend/src/pages/user/ProductDetail.vue b/frontend/src/pages/user/ProductDetail.vue index 57f8694..cdbbef1 100644 --- a/frontend/src/pages/user/ProductDetail.vue +++ b/frontend/src/pages/user/ProductDetail.vue @@ -5,7 +5,7 @@ @@ -39,6 +39,7 @@ const product = ref({}) const images = ref([]) const quantity = ref(1) +const baseUrl = 'http://localhost:8080' const load = async () => { loading.value = true try { @@ -61,6 +62,12 @@ const addToCart = async () => { message.success('已加入购物车') } +const previewUrl = (url) => { + if (!url) return '' + if (url.startsWith('http')) return url + return `${baseUrl}${url}` +} + onMounted(load)