diff --git a/backend/src/main/java/com/car/config/WebConfig.java b/backend/src/main/java/com/car/config/WebConfig.java index 80f501c..f0691a3 100644 --- a/backend/src/main/java/com/car/config/WebConfig.java +++ b/backend/src/main/java/com/car/config/WebConfig.java @@ -1,11 +1,18 @@ package com.car.config; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { + private final String uploadDir; + + public WebConfig(org.springframework.core.env.Environment environment) { + this.uploadDir = environment.getProperty("file.upload-dir", ""); + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -14,4 +21,12 @@ public class WebConfig implements WebMvcConfigurer { .allowedHeaders("*") .allowCredentials(true); } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + if (!uploadDir.isBlank()) { + String location = "file:" + (uploadDir.endsWith("/") ? uploadDir : uploadDir + "/"); + registry.addResourceHandler("/uploads/**").addResourceLocations(location); + } + } } diff --git a/backend/src/main/java/com/car/controller/UploadController.java b/backend/src/main/java/com/car/controller/UploadController.java new file mode 100644 index 0000000..cd15781 --- /dev/null +++ b/backend/src/main/java/com/car/controller/UploadController.java @@ -0,0 +1,38 @@ +package com.car.controller; + +import com.car.common.ApiResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +@RestController +@RequestMapping("/api") +public class UploadController { + + @Value("${file.upload-dir}") + private String uploadDir; + + @PostMapping("/upload") + public ApiResponse upload(@RequestParam("file") MultipartFile file) throws IOException { + if (file.isEmpty()) { + return new ApiResponse<>(400, "文件不能为空", null); + } + File dir = new File(uploadDir); + if (!dir.exists() && !dir.mkdirs()) { + return new ApiResponse<>(500, "上传目录创建失败", null); + } + String ext = StringUtils.getFilenameExtension(file.getOriginalFilename()); + String filename = UUID.randomUUID().toString().replace("-", ""); + if (ext != null && !ext.isBlank()) { + filename += "." + ext; + } + File dest = new File(dir, filename); + file.transferTo(dest); + return ApiResponse.ok("/uploads/" + filename); + } +} diff --git a/backend/src/main/java/com/car/service/UserService.java b/backend/src/main/java/com/car/service/UserService.java index c2f115b..86ec4cd 100644 --- a/backend/src/main/java/com/car/service/UserService.java +++ b/backend/src/main/java/com/car/service/UserService.java @@ -74,6 +74,12 @@ public class UserService { if (db == null) { throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在"); } + if ("PENDING".equals(db.getRealNameStatus())) { + throw new ApiException(ErrorCode.CONFLICT, "实名认证正在审核中"); + } + if ("APPROVED".equals(db.getRealNameStatus())) { + throw new ApiException(ErrorCode.CONFLICT, "实名认证已通过"); + } db.setRealName(request.getRealName()); db.setIdNumber(request.getIdNumber()); db.setIdFront(request.getIdFront()); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 8d8c2b7..b3e1582 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -3,9 +3,9 @@ server: spring: datasource: - url: jdbc:mysql://localhost:3306/car_rental?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai + url: jdbc:mysql://localhost:3307/car_rental?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root - password: root + password: qq5211314 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai @@ -24,3 +24,6 @@ sa-token: is-share: true token-prefix: Bearer is-log: false + +file: + upload-dir: ${user.dir}/uploads diff --git a/backend/uploads/02835d06b6604f61b71962f88dc733f1.png b/backend/uploads/02835d06b6604f61b71962f88dc733f1.png new file mode 100644 index 0000000..2220c31 Binary files /dev/null and b/backend/uploads/02835d06b6604f61b71962f88dc733f1.png differ diff --git a/backend/uploads/d5e5539a596a40dabac9e6023c3ae84c.png b/backend/uploads/d5e5539a596a40dabac9e6023c3ae84c.png new file mode 100644 index 0000000..2220c31 Binary files /dev/null and b/backend/uploads/d5e5539a596a40dabac9e6023c3ae84c.png differ diff --git a/frontend/src/api/http.js b/frontend/src/api/http.js index 6febf9e..849f0be 100644 --- a/frontend/src/api/http.js +++ b/frontend/src/api/http.js @@ -1,4 +1,5 @@ import axios from 'axios' +import { Message } from '@arco-design/web-vue' import { useAuthStore } from '../store/auth' const http = axios.create({ @@ -18,11 +19,16 @@ http.interceptors.response.use( (response) => { const res = response.data if (res.code !== 0) { - return Promise.reject(new Error(res.message || '请求失败')) + Message.error(res.message || 'Request failed') + return Promise.reject(new Error(res.message || 'Request failed')) } return res.data }, - (error) => Promise.reject(error) + (error) => { + const message = error?.response?.data?.message || error.message || 'Request failed' + Message.error(message) + return Promise.reject(error) + } ) export default http diff --git a/frontend/src/style.css b/frontend/src/style.css index e19e094..eb924d0 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -20,12 +20,14 @@ a { gap: 24px; background: #ffffff; border-bottom: 1px solid #e5e6eb; + flex-wrap: nowrap; } .logo { font-weight: 600; font-size: 18px; padding: 0 16px; + white-space: nowrap; } .user-actions { @@ -34,6 +36,17 @@ a { display: flex; align-items: center; gap: 12px; + white-space: nowrap; +} + +.header .arco-menu { + flex: 1; + min-width: 0; +} + +.header .arco-menu-horizontal { + display: flex; + flex-wrap: nowrap; } .divider { diff --git a/frontend/src/utils/format.js b/frontend/src/utils/format.js new file mode 100644 index 0000000..6c8e322 --- /dev/null +++ b/frontend/src/utils/format.js @@ -0,0 +1,31 @@ +export const roleMap = { + ADMIN: '管理员', + USER: '用户' +} + +export const userStatusMap = { + ACTIVE: '启用', + DISABLED: '禁用' +} + +export const realNameStatusMap = { + NONE: '未提交', + PENDING: '审核中', + APPROVED: '已通过', + REJECTED: '已驳回' +} + +export const orderStatusMap = { + PENDING_PAY: '待支付', + RENTING: '租用中', + RETURNED: '已归还', + CANCELLED: '已取消' +} + +export const carStatusMap = { + AVAILABLE: '可租', + RENTED: '出租中', + MAINTENANCE: '维护中' +} + +export const mapText = (map, value) => map[value] || value || '-' diff --git a/frontend/src/views/CarDetail.vue b/frontend/src/views/CarDetail.vue index aadf13f..2f21562 100644 --- a/frontend/src/views/CarDetail.vue +++ b/frontend/src/views/CarDetail.vue @@ -8,7 +8,7 @@

里程:{{ car.mileage }} km

{{ car.description }}

特价 - 不可租 + {{ mapText(carStatusMap, car.status) }}
{{ isFavorite ? '取消收藏' : '加入收藏' }} @@ -37,6 +37,7 @@ import { useRoute, useRouter } from 'vue-router' import { Message } from '@arco-design/web-vue' import http from '../api/http' import { useAuthStore } from '../store/auth' +import { carStatusMap, mapText } from '../utils/format' const route = useRoute() const router = useRouter() diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index d759913..a162931 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -26,7 +26,7 @@

日租金:¥{{ car.pricePerDay }}

押金:¥{{ car.deposit }}

特价 - 不可租 + {{ mapText(carStatusMap, car.status) }}
查看详情 @@ -40,6 +40,7 @@ diff --git a/frontend/src/views/admin/AdminCars.vue b/frontend/src/views/admin/AdminCars.vue index 4a01335..dbb0ede 100644 --- a/frontend/src/views/admin/AdminCars.vue +++ b/frontend/src/views/admin/AdminCars.vue @@ -83,6 +83,7 @@ import { ref } from 'vue' import { Message } from '@arco-design/web-vue' import http from '../../api/http' +import { carStatusMap, mapText } from '../../utils/format' const cars = ref([]) const query = ref({ keyword: '', isSpecial: null }) @@ -95,8 +96,8 @@ const columns = [ { title: '车型', dataIndex: 'model' }, { title: '车牌', dataIndex: 'plateNo' }, { title: '日租金', dataIndex: 'pricePerDay' }, - { title: '状态', dataIndex: 'status' }, - { title: '特价', dataIndex: 'isSpecial' }, + { title: '状态', dataIndex: 'statusText' }, + { title: '特价', dataIndex: 'isSpecialText' }, { title: '操作', slotName: 'actions' } ] @@ -104,7 +105,12 @@ const loadCars = async () => { const params = {} if (query.value.keyword) params.keyword = query.value.keyword if (query.value.isSpecial !== null) params.isSpecial = query.value.isSpecial - cars.value = await http.get('/api/admin/cars', { params }) + const list = await http.get('/api/admin/cars', { params }) + cars.value = list.map((car) => ({ + ...car, + statusText: mapText(carStatusMap, car.status), + isSpecialText: car.isSpecial ? '是' : '否' + })) } const openEdit = (record) => { diff --git a/frontend/src/views/admin/AdminOrders.vue b/frontend/src/views/admin/AdminOrders.vue index a0a9d14..7457898 100644 --- a/frontend/src/views/admin/AdminOrders.vue +++ b/frontend/src/views/admin/AdminOrders.vue @@ -25,25 +25,42 @@