Explorar el Código

feat: 订单管理模块增强(分页/状态枚举/工具类)

docker hace 2 meses
padre
commit
a7ef42e759

+ 0 - 5
backend/src/main/java/com/oms/controller/ChannelController.java

@@ -22,11 +22,6 @@ public class ChannelController {
         return channelService.getPage(page, size).getRecords();
     }
 
-    @GetMapping("/all")
-    public List<Channel> getAll() {
-        return channelService.getAll();
-    }
-
     @GetMapping("/{id}")
     public ChannelDTO getById(@PathVariable Long id) {
         return channelService.getDtoById(id);

+ 27 - 9
backend/src/main/java/com/oms/controller/OrdersController.java

@@ -1,12 +1,18 @@
 package com.oms.controller;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.oms.converter.OrdersConverter;
+import com.oms.dto.OrderListDTO;
 import com.oms.dto.OrdersDTO;
+import com.oms.dto.PageResponse;
 import com.oms.entity.Orders;
+import com.oms.service.ChannelService;
 import com.oms.service.OrdersService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/order/orders")
@@ -14,17 +20,24 @@ import java.util.List;
 public class OrdersController {
 
     private final OrdersService ordersService;
+    private final ChannelService channelService;
+    private final OrdersConverter ordersConverter;
 
     @GetMapping
-    public List<Orders> getOrders(
+    public PageResponse<OrderListDTO> getOrders(
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "20") int size) {
-        return ordersService.getPage(page, size).getRecords();
-    }
+        Page<Orders> pageResult = ordersService.getPage(page, size);
+        Map<Long, String> channelNames = channelService.getChannelNameMap();
+
+        List<OrderListDTO> items = pageResult.getRecords().stream().map(o -> {
+            OrderListDTO dto = ordersConverter.toListDto(o);
+            dto.setChannel(channelNames.getOrDefault(o.getChannelId(), "渠道" + o.getChannelId()));
+            dto.setItems(List.of());
+            return dto;
+        }).toList();
 
-    @GetMapping("/all")
-    public List<Orders> getAll() {
-        return ordersService.getAll();
+        return PageResponse.of(items, pageResult.getTotal(), (int) pageResult.getCurrent(), (int) pageResult.getSize());
     }
 
     @GetMapping("/{id}")
@@ -33,8 +46,8 @@ public class OrdersController {
     }
 
     @GetMapping("/order-no/{orderNo}")
-    public Orders getByOrderNo(@PathVariable String orderNo) {
-        return ordersService.getByOrderNo(orderNo);
+    public OrdersDTO getByOrderNo(@PathVariable String orderNo) {
+        return ordersService.getDtoByOrderNo(orderNo);
     }
 
     @PostMapping
@@ -84,7 +97,7 @@ public class OrdersController {
     }
 
     @PostMapping("/{id}/split")
-    public Long splitOrder(@PathVariable Long id, @RequestBody List<java.util.Map<String, Object>> splits) {
+    public Long splitOrder(@PathVariable Long id, @RequestBody List<Map<String, Object>> splits) {
         return ordersService.splitOrder(id, splits, "SYSTEM");
     }
 
@@ -102,4 +115,9 @@ public class OrdersController {
     public void assignWarehouse(@PathVariable Long id, @RequestParam Long warehouseId) {
         ordersService.assignWarehouse(id, warehouseId);
     }
+
+    @PostMapping("/sync-random")
+    public Orders syncRandomOrder() {
+        return ordersService.createRandomOrder();
+    }
 }

+ 3 - 0
backend/src/main/java/com/oms/converter/OrdersConverter.java

@@ -1,5 +1,6 @@
 package com.oms.converter;
 
+import com.oms.dto.OrderListDTO;
 import com.oms.dto.OrdersDTO;
 import com.oms.entity.Orders;
 import org.mapstruct.Mapper;
@@ -9,4 +10,6 @@ import org.mapstruct.Mapping;
 public interface OrdersConverter extends BaseConverter<Orders, OrdersDTO> {
     @Mapping(target = "id", ignore = true)
     Orders toEntity(OrdersDTO dto);
+
+    OrderListDTO toListDto(Orders entity);
 }

+ 36 - 0
backend/src/main/java/com/oms/dto/OrderListDTO.java

@@ -0,0 +1,36 @@
+package com.oms.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class OrderListDTO {
+    private Long id;
+    private String orderNo;
+    private String channelOrderNo;
+    private Long channelId;
+    private String channel;
+    private String orderStatus;
+    private String shippingStatus;
+    private String paymentStatus;
+    private String exceptionTag;
+    private String priority;
+    private String buyer;
+    private String buyerEmail;
+    private String buyerPhone;
+    private String buyerCountry;
+    private String buyerLevel;
+    private String receiverName;
+    private String receiverCountry;
+    private String receiverCity;
+    private String receiverPhone;
+    private Integer itemCount;
+    private BigDecimal orderAmount;
+    private BigDecimal actualPaid;
+    private String warehouseLocation;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    private List<OrderItemDTO> items;
+}

+ 20 - 0
backend/src/main/java/com/oms/dto/PageResponse.java

@@ -0,0 +1,20 @@
+package com.oms.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PageResponse<T> {
+    private List<T> items;
+    private long total;
+    private int page;
+    private int size;
+
+    public static <T> PageResponse<T> of(List<T> items, long total, int page, int size) {
+        return new PageResponse<>(items, total, page, size);
+    }
+}

+ 11 - 0
backend/src/main/java/com/oms/enums/OrderStatus.java

@@ -0,0 +1,11 @@
+package com.oms.enums;
+
+public enum OrderStatus {
+    CREATED,
+    PAID,
+    ALLOCATED,
+    SHIPPED,
+    DELIVERED,
+    COMPLETED,
+    CANCELLED
+}

+ 7 - 0
backend/src/main/java/com/oms/enums/PaymentStatus.java

@@ -0,0 +1,7 @@
+package com.oms.enums;
+
+public enum PaymentStatus {
+    UNPAID,
+    PENDING,
+    PAID
+}

+ 9 - 0
backend/src/main/java/com/oms/enums/ShippingStatus.java

@@ -0,0 +1,9 @@
+package com.oms.enums;
+
+public enum ShippingStatus {
+    UNSHIPPED,
+    PROCESSING,
+    SHIPPED,
+    IN_TRANSIT,
+    DELIVERED
+}

+ 4 - 1
backend/src/main/java/com/oms/service/ChannelService.java

@@ -19,8 +19,11 @@ public class ChannelService {
     private final ChannelConverter converter;
 
     public Page<Channel> getPage(int page, int size) { return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<Channel>().orderByDesc(Channel::getCreatedAt)); }
-    public java.util.List<Channel> getAll() { return mapper.selectList(new LambdaQueryWrapper<Channel>().orderByDesc(Channel::getCreatedAt)); }
     public Channel getById(Long id) { return mapper.selectById(id); }
+    public java.util.Map<Long, String> getChannelNameMap() {
+        return mapper.selectList(null).stream()
+                .collect(java.util.stream.Collectors.toMap(Channel::getId, Channel::getChannelName));
+    }
     public ChannelDTO getDtoById(Long id) { Channel e = mapper.selectById(id); return e == null ? null : converter.toDto(e); }
     public Long save(Channel entity) { mapper.insert(entity); return entity.getId(); }
     public void update(Channel entity) { mapper.updateById(entity); }

+ 74 - 0
backend/src/main/java/com/oms/service/OrdersService.java

@@ -58,6 +58,11 @@ public class OrdersService {
         return mapper.selectOne(new LambdaQueryWrapper<Orders>().eq(Orders::getOrderNo, orderNo));
     }
 
+    public OrdersDTO getDtoByOrderNo(String orderNo) {
+        Orders e = getByOrderNo(orderNo);
+        return e == null ? null : converter.toDto(e);
+    }
+
     public Long save(Orders entity) { mapper.insert(entity); return entity.getId(); }
 
     public void update(Orders entity) { mapper.updateById(entity); }
@@ -219,4 +224,73 @@ public class OrdersService {
         o.setWarehouseId(warehouseId);
         mapper.updateById(o);
     }
+
+    private final String[] COUNTRIES = {"US", "UK", "JP", "DE", "FR", "CA"};
+    private final String[] BUYERS = {"Olivia Zhang", "Noah Smith", "Liam Chen", "Emma Wilson", "Sophie Brown", "James Wang", "Lisa Johnson", "David Lee"};
+    private final String[] WAREHOUSES = {"深圳南山仓", "义乌商贸仓", "洛杉矶海外仓"};
+
+    @Transactional
+    public Orders createRandomOrder() {
+        Orders order = new Orders();
+        order.setOrderNo("OMS-" + System.currentTimeMillis());
+        order.setChannelOrderNo("CH" + System.currentTimeMillis());
+        order.setChannelId((long) (Math.random() * 3 + 1));
+        order.setOrderStatus("CREATED");
+        order.setShippingStatus("UNSHIPPED");
+        order.setPaymentStatus("UNPAID");
+        order.setRefundStatus("NONE");
+        order.setExceptionTag(Math.random() > 0.85 ? "地址需复核" : null);
+        order.setPriority(Math.random() > 0.9 ? "URGENT" : "NORMAL");
+
+        int buyerIdx = (int) (Math.random() * BUYERS.length);
+        order.setBuyer(BUYERS[buyerIdx]);
+        order.setBuyerId("buyer-" + System.currentTimeMillis());
+        order.setBuyerEmail(BUYERS[buyerIdx].toLowerCase().replace(" ", ".") + "@mail.com");
+        order.setBuyerPhone("+1" + (int)(Math.random() * 9000000000L + 1000000000L));
+
+        int countryIdx = (int) (Math.random() * COUNTRIES.length);
+        order.setBuyerCountry(COUNTRIES[countryIdx]);
+        order.setReceiverCountry(COUNTRIES[countryIdx]);
+        order.setReceiverName(BUYERS[buyerIdx]);
+        order.setReceiverPhone("+1" + (int)(Math.random() * 9000000000L + 1000000000L));
+        order.setReceiverCity("City-" + countryIdx);
+        order.setReceiverState("State-" + countryIdx);
+        order.setReceiverPostalCode(String.valueOf(10000 + (int)(Math.random() * 90000)));
+        order.setReceiverAddress("123 Main Street, City " + countryIdx + ", Country " + COUNTRIES[countryIdx]);
+
+        order.setBuyerLevel(new String[]{"VIP", "黄金", "普通"}[(int)(Math.random() * 3)]);
+        order.setBuyerOrderCount((int)(Math.random() * 50));
+        order.setBuyerTotalSpent(BigDecimal.valueOf(Math.random() * 5000));
+
+        order.setCurrency("USD");
+        order.setExchangeRate(BigDecimal.ONE);
+        order.setPaymentMethod(new String[]{"PayPal", "Credit Card", "Bank Transfer"}[(int)(Math.random() * 3)]);
+
+        BigDecimal orderAmount = BigDecimal.valueOf(Math.random() * 200 + 20);
+        order.setOrderAmount(orderAmount);
+        order.setOrderAmountCny(orderAmount.multiply(BigDecimal.valueOf(7.2)));
+        order.setTaxAmount(orderAmount.multiply(BigDecimal.valueOf(0.08)));
+        order.setShippingFee(BigDecimal.valueOf(Math.random() * 15 + 5));
+        order.setDiscountAmount(BigDecimal.ZERO);
+        order.setActualPaid(orderAmount.add(order.getTaxAmount()).add(order.getShippingFee()));
+
+        order.setChannelId((long) (Math.random() * 5 + 1));
+        order.setWarehouseId((long) (Math.random() * 3 + 1));
+        order.setWarehouseLocation(WAREHOUSES[(int)(Math.random() * WAREHOUSES.length)]);
+
+        order.setDevice(new String[]{"Mobile", "Desktop", "Tablet"}[(int)(Math.random() * 3)]);
+        order.setIp("192.168." + (int)(Math.random() * 255) + "." + (int)(Math.random() * 255));
+        order.setIpCountry(COUNTRIES[countryIdx]);
+
+        order.setItemCount(1);
+        order.setTotalAmount(order.getActualPaid());
+
+        order.setCreatedAt(LocalDateTime.now());
+        order.setUpdatedAt(LocalDateTime.now());
+        order.setTenantId("DEFAULT");
+        order.setDeleted(0);
+
+        mapper.insert(order);
+        return order;
+    }
 }

+ 20 - 0
backend/src/main/java/com/oms/util/PageUtil.java

@@ -0,0 +1,20 @@
+package com.oms.util;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.oms.dto.PageResponse;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class PageUtil {
+
+    public static <T, R> PageResponse<R> convert(Page<T> page, Function<T, R> converter) {
+        List<R> items = page.getRecords().stream().map(converter).collect(Collectors.toList());
+        return PageResponse.of(items, page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
+    }
+
+    public static <T> PageResponse<T> of(Page<T> page) {
+        return PageResponse.of(page.getRecords(), page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
+    }
+}

+ 6 - 1
backend/src/main/resources/application.yml

@@ -6,9 +6,14 @@ server:
 spring:
   application:
     name: oms-backend
+  web:
+    encoding:
+      charset: UTF-8
+      force-response: true
+      force-request: true
 
   datasource:
-    url: jdbc:mysql://localhost:3306/oms?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
+    url: jdbc:mysql://localhost:3306/oms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
     username: root
     password: root
     driver-class-name: com.mysql.cj.jdbc.Driver

+ 1 - 0
frontend/src/api/services.ts

@@ -267,6 +267,7 @@ export const api = {
   getOrderByOrderNo: (orderNo: string) => request<OrderItem>(`/api/order/orders/order-no/${orderNo}`),
   createOrder: (data: any) =>
     request<OrderItem>('/api/order/orders', { method: 'POST', body: JSON.stringify(data) }),
+  syncRandomOrder: () => request<any>('/api/order/orders/sync-random', { method: 'POST' }),
   updateOrder: (id: number, data: any) =>
     request<OrderItem>(`/api/order/orders/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
   deleteOrder: (id: number) =>

+ 2 - 2
frontend/src/layout/AppLayout.vue

@@ -5,7 +5,7 @@
         <div class="brand__mark">CB</div>
         <div v-if="!appStore.menuCollapsed" class="brand__meta">
           <strong>CrossBorder OS</strong>
-          <span>Vue 3 + TS + Mock</span>
+          <span>Vue 3 + TS</span>
         </div>
       </div>
 
@@ -50,7 +50,7 @@
             <el-button plain @click="appStore.toggleMenu()">
               {{ appStore.menuCollapsed ? '展开菜单' : '收起菜单' }}
             </el-button>
-            <el-tag effect="dark" type="success">Mock 环境</el-tag>
+            <el-tag effect="dark" type="success">API 已对接</el-tag>
             <el-dropdown>
               <div class="header-user glass-card">
                 <div class="header-user__avatar">{{ authStore.user?.avatar || 'CB' }}</div>

+ 2 - 2
frontend/src/stores/auth.ts

@@ -24,7 +24,7 @@ export const useAuthStore = defineStore('auth', {
         this.token = res.token;
         this.user = {
           name: res.name,
-          role: res.role as UserRole,
+          role: (res.role as string).toLowerCase() as UserRole,
           roleLabel: res.roleLabel,
           workspace: '',
           avatar: res.avatar
@@ -50,7 +50,7 @@ export const useAuthStore = defineStore('auth', {
         const res = await api.getCurrentUser();
         this.user = {
           name: res.name || res.username || '',
-          role: (res.role || 'OPERATOR') as UserRole,
+          role: ((res.role || 'OPERATOR') as string).toLowerCase() as UserRole,
           roleLabel: res.roleLabel || '',
           workspace: res.workspace || '',
           avatar: res.avatar || ''

+ 6 - 1
frontend/src/views/order/OrderDetailView.vue

@@ -626,7 +626,12 @@ const loadData = async () => {
   const id = route.query.id as string;
   if (!id) return;
 
-  Object.assign(order, generateMockOrderDetail(id));
+  try {
+    const res = await api.getOrder(Number(id));
+    Object.assign(order, res);
+  } catch (e) {
+    console.error('Failed to load order:', e);
+  }
 };
 
 const generateMockOrderDetail = (id: string): Partial<OrderItem> => {

+ 17 - 2
frontend/src/views/order/OrderListView.vue

@@ -401,7 +401,11 @@ const rowClass = ({ row }: { row: OrderItem }) => row.exceptionTag !== '正常'
 const loadData = async () => {
   loading.value = true;
   try {
-    items.value = generateMockOrders();
+    const res = await api.getOrders();
+    items.value = res.items || [];
+  } catch (e) {
+    console.error('Failed to load orders:', e);
+    items.value = [];
   } finally {
     loading.value = false;
   }
@@ -466,7 +470,18 @@ const generateMockOrders = (): OrderItem[] => {
 
 const resetFilters = () => { filters.value = { orderNo: '', buyer: '', channel: '', warehouse: '', orderStatus: '', paymentStatus: '', shippingStatus: '', exceptionTag: '', buyerCountry: '', dateRange: [] }; };
 const onSelection = (rows: OrderItem[]) => { selected.value = rows; };
-const refreshSync = () => { ElMessage.success('同步完成:新增 3 单'); loadData(); };
+const refreshSync = async () => {
+  try {
+    const count = Math.floor(Math.random() * 3) + 1;
+    for (let i = 0; i < count; i++) {
+      await api.syncRandomOrder();
+    }
+    ElMessage.success(`同步完成:新增 ${count} 单`);
+    loadData();
+  } catch (e) {
+    ElMessage.error('同步失败');
+  }
+};
 const doExport = () => { ElMessage.success('导出已开始'); };
 
 const handleBatchCommand = (cmd: string) => {

+ 1 - 0
frontend/src/views/product/ProductListView.vue

@@ -168,6 +168,7 @@ const categories = ['Luggage', 'Bags', 'Sports', 'Outdoor'];
 const brands = ['NomadPeak', 'UrbanTrail', 'AeroDry'];
 
 const filteredItems = computed(() => {
+  if (!items.value) return [];
   return items.value.filter((item) => {
     if (filters.value.keyword && !item.title.includes(filters.value.keyword) && !item.spu.includes(filters.value.keyword)) return false;
     if (filters.value.category && item.category !== filters.value.category) return false;

+ 2 - 2
frontend/src/views/report/ReportCenterView.vue

@@ -217,10 +217,10 @@ const reportData = ref<ReportDataItem[]>([]);
 const loading = ref(false);
 const reportDataPage = ref(1);
 const reportDataPageSize = ref(10);
-const reportDataTotal = computed(() => reportData.value.length);
+const reportDataTotal = computed(() => reportData.value?.length ?? 0);
 const reportListPage = ref(1);
 const reportListPageSize = ref(10);
-const reportListTotal = computed(() => reportList.value.length);
+const reportListTotal = computed(() => reportList.value?.length ?? 0);
 
 const filters = ref({
   timeRange: null as [Date, Date] | null,