From 3e47804f7ef9576dac0755c0feda5032a1b820c5 Mon Sep 17 00:00:00 2001 From: dzq Date: Sat, 29 Nov 2025 17:41:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=80=9F=E8=BF=98=E5=8A=A8=E6=80=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在订单模块中添加借还动态接口及相关类型定义 - 创建借还动态接口文档,详细说明接口使用方式 - 在欢迎页面新增借还动态展示区域,使用时间轴形式展示借出和归还记录 - 实现动态记录的筛选、状态显示和图片预览功能 --- doc/借还动态接口文档.md | 245 +++++++++++++++++++++ src/api/shop/order.ts | 81 +++++++ src/views/welcome/index.vue | 424 ++++++++++++++++++++++++++++++++++-- 3 files changed, 731 insertions(+), 19 deletions(-) create mode 100644 doc/借还动态接口文档.md diff --git a/doc/借还动态接口文档.md b/doc/借还动态接口文档.md new file mode 100644 index 0000000..4a5fbed --- /dev/null +++ b/doc/借还动态接口文档.md @@ -0,0 +1,245 @@ +# 借还动态接口文档 + +## 概述 + +本文档介绍新增加的借还动态列表接口,该接口将借出和归还操作分别作为独立记录返回,相比原有的 `getBorrowReturnRecordList` 接口,能更清晰地展示订单的生命周期。 + +## 接口信息 + +### 新增接口 + +**接口地址**: `GET /shop/order/borrow-return-dynamic` + +**功能描述**: 查询借还动态分页列表,返回借出和归还的独立记录 + +## 数据结构 + +### 请求参数 + +#### SearchBorrowReturnDynamicQuery + +| 字段名 | 类型 | 是否必填 | 说明 | +|--------|------|----------|------| +| goodsId | Long | 否 | 商品ID,精确筛选 | +| status | Integer | 否 | 状态筛选(仅对归还记录有效)
- 0: 未退还
- 1: 待审批
- 2: 已通过
- 3: 已驳回
- 4: 开柜中 | +| dynamicType | Integer | 否 | 动态类型筛选
- 0: 借出记录
- 1: 归还记录 | +| pageNum | Integer | 否 | 页码(默认1) | +| pageSize | Integer | 否 | 每页大小(默认10) | + +### 响应数据 + +#### BorrowReturnDynamicDTO + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| orderGoodsId | Long | 订单商品ID | +| dynamicType | Integer | 动态类型(0-借出 1-归还) | +| dynamicTypeStr | String | 动态类型描述 | +| orderId | Long | 订单ID | +| orderTime | Date | 订单创建时间/借出时间 | +| goodsId | Long | 商品ID | +| goodsName | String | 商品名称 | +| goodsPrice | BigDecimal | 商品单价 | +| quantity | Integer | 数量 | +| paymentMethod | String | 支付方式 | +| orderName | String | 订单姓名 | +| orderMobile | String | 订单手机号 | +| cellId | Long | 格口ID | +| cellNo | Integer | 格口号 | +| cabinetId | Long | 柜机ID | +| cabinetName | String | 柜机名称 | +| operateTime | Date | 归还/审批时间(归还记录时有效) | +| approvalId | Long | 审批ID(归还记录时有效) | +| status | Integer | 审批状态(归还记录时有效) | +| statusStr | String | 状态描述 | +| auditName | String | 审批人(归还记录时有效) | +| auditRemark | String | 审核说明(归还记录时有效) | +| images | String | 归还图片(归还记录时有效) | + +## 业务说明 + +### 记录分类 + +每条订单商品会产生两条动态记录: + +1. **借出记录** (`dynamicType = 0`) + - 记录时间:`orderTime` = 订单创建时间 + - 状态:固定为 0(未退还) + - 审批相关字段:均为 null + +2. **归还记录** (`dynamicType = 1`) + - 记录时间:`operateTime` = 审批时间(若无则为创建时间) + - 状态:实际的审批状态 + - 审批相关字段:审批人、审核说明、归还图片等 + +### 与原接口对比 + +| 特性 | getBorrowReturnRecordList | getBorrowReturnDynamicList | +|------|---------------------------|----------------------------| +| 记录方式 | 一条记录包含借出和归还信息 | 借出和归还分别独立记录 | +| 时间展示 | 只显示一个时间 | 分别显示借出时间和归还时间 | +| 状态展示 | 状态混合显示 | 归还记录显示审批状态,借出记录固定为未退还 | +| 数据清晰度 | 信息集中但复杂 | 信息分离但清晰 | +| 适用场景 | 需要快速查看整体情况 | 需要追踪完整操作流程 | + +## 使用示例 + +### 示例1: 查询所有动态记录 + +**请求**: +```http +GET /shop/order/borrow-return-dynamic +``` + +**响应**: +```json +{ + "code": 200, + "msg": "成功", + "data": { + "records": [ + { + "orderGoodsId": 1001, + "dynamicType": 0, + "dynamicTypeStr": "借出", + "orderId": 1000, + "orderTime": "2025-11-28 10:00:00", + "goodsId": 200, + "goodsName": "iPad", + "goodsPrice": 3000.00, + "quantity": 1, + "paymentMethod": "wechat", + "orderName": "张三", + "orderMobile": "13800138000", + "cellId": 1, + "cellNo": 5, + "cabinetId": 1, + "cabinetName": "主柜A", + "operateTime": null, + "approvalId": null, + "status": 0, + "statusStr": "未退还", + "auditName": null, + "auditRemark": null, + "images": null + }, + { + "orderGoodsId": 1001, + "dynamicType": 1, + "dynamicTypeStr": "归还", + "orderId": 1000, + "orderTime": "2025-11-28 10:00:00", + "goodsId": 200, + "goodsName": "iPad", + "goodsPrice": 3000.00, + "quantity": 1, + "paymentMethod": "wechat", + "orderName": "张三", + "orderMobile": "13800138000", + "cellId": 1, + "cellNo": 5, + "cabinetId": 1, + "cabinetName": "主柜A", + "operateTime": "2025-11-28 14:00:00", + "approvalId": 500, + "status": 2, + "statusStr": "已通过", + "auditName": "李四", + "auditRemark": "商品完好", + "images": "/images/return/123.jpg" + } + ], + "total": 2, + "size": 10, + "current": 1, + "orders": [], + "hitCount": false, + "searchCount": true, + "pages": 1 + } +} +``` + +### 示例2: 仅查询借出记录 + +**请求**: +```http +GET /shop/order/borrow-return-dynamic?dynamicType=0 +``` + +### 示例3: 查询已通过的归还记录 + +**请求**: +```http +GET /shop/order/borrow-return-dynamic?dynamicType=1&status=2 +``` + +### 示例4: 按商品筛选 + +**请求**: +```http +GET /shop/order/borrow-return-dynamic?goodsId=200 +``` + +## 实现细节 + +### 数据库查询 + +接口使用 `UNION ALL` 查询实现: + +1. 第一部分:查询借出记录 + - 来源:`shop_order` + `shop_order_goods` + `cabinet_cell` + `smart_cabinet` + - 时间字段:`so.create_time` + - 状态:固定值 0 + +2. 第二部分:查询归还记录 + - 来源:`shop_order` + `shop_order_goods` + `return_approval` + `cabinet_cell` + `smart_cabinet` + - 时间字段:`COALESCE(ra.approval_time, ra.create_time)` + - 状态:实际审批状态 + +### 服务层实现 + +- **Mapper**: `ShopOrderMapper.getBorrowReturnDynamicList()` +- **Service**: `ShopOrderService.getBorrowReturnDynamicList()` +- **ApplicationService**: `OrderApplicationService.getBorrowReturnDynamicList()` +- **Controller**: `ShopOrderController.getBorrowReturnDynamicList()` + +### 分页处理 + +使用 MyBatis-Plus 的 `Page` 插件实现分页查询,支持: +- 页码控制(pageNum) +- 每页大小控制(pageSize) +- 总记录数统计 + +## 注意事项 + +1. **状态筛选**: `status` 参数仅对 `dynamicType=1`(归还记录)有效,对借出记录无效 +2. **空值处理**: 借出记录的审批相关字段(approvalId、auditName、auditRemark、images)均为 null +3. **时间排序**: 查询结果按动态发生时间(dynamic_time)倒序排列 +4. **数据权限**: 接口遵循现有的数据权限控制逻辑 +5. **性能考虑**: 大量数据时建议使用分页,并合理设置筛选条件 + +## 相关文件 + +### 新增文件 + +- `agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/BorrowReturnDynamicDTO.java` +- `agileboot-domain/src/main/java/com/agileboot/domain/shop/order/query/SearchBorrowReturnDynamicQuery.java` + +### 修改文件 + +- `agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderMapper.java` +- `agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderService.java` +- `agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderServiceImpl.java` +- `agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java` +- `agileboot-admin/src/main/java/com/agileboot/admin/controller/shop/ShopOrderController.java` + +## 更新日志 + +| 日期 | 版本 | 更新内容 | +|------|------|----------| +| 2025-11-28 | v1.0 | 新增借还动态接口,支持借出和归还分别独立查询 | + +## 联系信息 + +如有问题或建议,请联系开发团队。 diff --git a/src/api/shop/order.ts b/src/api/shop/order.ts index a475bd4..31b8a6a 100644 --- a/src/api/shop/order.ts +++ b/src/api/shop/order.ts @@ -203,4 +203,85 @@ export const getBorrowReturnRecordListApi = (params?: BorrowReturnRecordQuery) = return http.request>>("get", "/shop/order/borrow-return-record", { params }); +}; + +// 借还动态接口相关定义 + +export interface BorrowReturnDynamicQuery extends BasePageQuery { + /** 商品ID */ + goodsId?: number; + /** 格口ID,精确筛选 */ + cellId?: number; + /** + * 状态筛选(仅对归还记录有效) + * @remarks + * 0-未退还 | 1-待审批 | 2-已通过 | 3-已驳回 | 4-开柜中 + */ + status?: number; + /** + * 动态类型筛选 + * @remarks + * 0-借出记录 | 1-归还记录 + */ + dynamicType?: number; +} + +export interface BorrowReturnDynamicDTO { + /** 订单商品ID */ + orderGoodsId: number; + /** 动态类型(0-借出 1-归还) */ + dynamicType: number; + /** 动态类型描述 */ + dynamicTypeStr: string; + /** 订单ID */ + orderId: number; + /** 订单创建时间/借出时间 */ + orderTime?: Date; + /** 商品ID */ + goodsId: number; + /** 商品名称 */ + goodsName: string; + /** 商品单价 */ + goodsPrice: number; + /** 数量 */ + quantity: number; + /** 商品封面图 */ + coverImg?: string; + /** 支付方式 */ + paymentMethod?: string; + /** 订单姓名 */ + orderName?: string; + /** 订单手机号 */ + orderMobile?: string; + /** 格口ID */ + cellId?: number; + /** 格口号 */ + cellNo?: number; + /** 柜机ID */ + cabinetId?: number; + /** 柜机名称 */ + cabinetName?: string; + /** 归还/审批时间(归还记录时有效) */ + operateTime?: Date; + /** 审批ID(归还记录时有效) */ + approvalId?: number; + /** 审批状态(归还记录时有效) */ + status?: number; + /** 状态描述 */ + statusStr?: string; + /** 审批人(归还记录时有效) */ + auditName?: string; + /** 审核说明(归还记录时有效) */ + auditRemark?: string; + /** 归还图片(归还记录时有效) */ + images?: string; + /** 审核图片(归还记录时有效) */ + auditImages?: string; +} + +/** 获取借还动态分页列表 */ +export const getBorrowReturnDynamicListApi = (params?: BorrowReturnDynamicQuery) => { + return http.request>>("get", "/shop/order/borrow-return-dynamic", { + params + }); }; \ No newline at end of file diff --git a/src/views/welcome/index.vue b/src/views/welcome/index.vue index 3ae000e..d6ff92c 100644 --- a/src/views/welcome/index.vue +++ b/src/views/welcome/index.vue @@ -2,6 +2,7 @@ import { Goods, Shop, Document, Money } from '@element-plus/icons-vue'; import { getStats, TodayLatestOrderGoodsDTO, TopGoodsDTO } from '@/api/shop/stats'; import { getShopListApi, ShopDTO, getModeText } from '@/api/shop/shop'; +import { getBorrowReturnDynamicListApi, BorrowReturnDynamicDTO } from '@/api/shop/order'; import { markRaw, onMounted, ref } from 'vue'; import { useWxStore } from '@/store/modules/wx'; import { ElDialog, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus'; @@ -43,6 +44,10 @@ const topGoods = ref([]); const todayLatestOrderGoods = ref([]); const maxOccurrenceCount = ref(0); +// 借还动态数据 +const borrowReturnDynamicList = ref([]); +const loading = ref(false); + const showDialog = ref(false); const form = ref({ name: '', idCard: '' }); @@ -79,7 +84,7 @@ const handleShopChange = (shopId: number) => { const fetchShopStats = async () => { if (!currentShop.value) return; - + try { const { data } = await getStats(wxStore.corpid); shopData.value = [ @@ -121,9 +126,69 @@ const fetchShopList = async () => { } }; +// 获取借还动态数据 +const fetchBorrowReturnDynamic = async () => { + loading.value = true; + try { + const { data } = await getBorrowReturnDynamicListApi({ + pageNum: 1, + pageSize: 20 + }); + if (data && data.rows) { + borrowReturnDynamicList.value = data.rows; + } + } catch (error) { + console.error('获取借还动态失败:', error); + ElMessage.error('获取借还动态失败'); + } finally { + loading.value = false; + } +}; + +// 格式化时间显示 +const formatTime = (time: Date | string) => { + if (!time) return ''; + const date = new Date(time); + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); +}; + +// 获取动态类型图标 +const getDynamicTypeIcon = (dynamicType: number) => { + return dynamicType === 0 ? 'el-icon-shopping-bag-1' : 'el-icon-refresh'; +}; + +// 获取动态类型颜色 +const getDynamicTypeColor = (dynamicType: number) => { + return dynamicType === 0 ? '#409EFF' : '#67C23A'; +}; + +// 获取状态颜色 +const getStatusColor = (status?: number) => { + if (status === undefined) return '#909399'; + switch (status) { + case 0: return '#909399'; // 未退还 + case 1: return '#E6A23C'; // 待审批 + case 2: return '#67C23A'; // 已通过 + case 3: return '#F56C6C'; // 已驳回 + case 4: return '#409EFF'; // 开柜中 + default: return '#909399'; + } +}; + +// 处理图片字符串,将逗号分隔的图片URL转换为数组 +const parseImages = (images?: string): string[] => { + if (!images) return []; + return images.split(',').map(img => img.trim()).filter(img => img.length > 0); +}; + onMounted(async () => { await fetchShopList(); - + try { const { data } = await getStats(wxStore.corpid); shopData.value = [ @@ -151,6 +216,8 @@ onMounted(async () => { console.error('获取统计数据失败:', error); } + // 获取借还动态数据 + await fetchBorrowReturnDynamic(); if (wxStore.qyUser && !wxStore.qyUser.ab98UserId) { showDialog.value = true; @@ -170,11 +237,8 @@ onMounted(async () => {
- +

{{ currentShop.shopName }}

@@ -196,11 +260,7 @@ onMounted(async () => {
- +

扫码进入小程序

@@ -303,7 +363,7 @@ onMounted(async () => { - + + + +
+
+
+
借还动态
+
+
+ + + +
+
+ + + + + {{ item.dynamicTypeStr }} + + + {{ item.statusStr }} + +
+ +
+ +
+ + + +
+ {{ item.goodsName }} + ¥{{ item.goodsPrice }} + +
+ {{ item.cabinetName }} + 格口{{ item.cellNo }} +
+
+
+ +
+ +
+ +
+
归还图片:
+
+ + + +
+
+ + +
+
审核图片:
+
+ + + +
+
+
+ +
+ 审批人: + {{ item.auditName }} +
+
+ 说明: + {{ item.auditRemark }} +
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+
+
@@ -557,13 +725,13 @@ onMounted(async () => { .shop-details { padding-top: 16px; - + .shop-name-container { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; - + .shop-name { font-size: 20px; font-weight: bold; @@ -571,19 +739,19 @@ onMounted(async () => { margin: 0; white-space: nowrap; } - + .shop-selector { min-width: 200px; - + :deep(.el-input__wrapper) { border-radius: 4px; border: 1px solid var(--el-border-color); box-shadow: none; - + &:hover { border-color: var(--el-color-primary); } - + &.is-focus { border-color: var(--el-color-primary); box-shadow: 0 0 0 1px var(--el-color-primary); @@ -622,5 +790,223 @@ onMounted(async () => { } } } + + // 借还动态样式 + .dynamic-container { + margin-bottom: 0; + height: 88vh; + overflow-y: auto; + + .dynamic-content { + .dynamic-timeline { + padding: 0; + + :deep(.el-timeline-item) { + .el-timeline-item__timestamp { + font-size: 12px; + color: var(--el-text-color-secondary); + margin-bottom: 8px; + } + + .el-timeline-item__node { + width: 12px; + height: 12px; + } + } + } + + .dynamic-card { + margin-bottom: 12px; + border: 1px solid var(--el-border-color); + + .dynamic-card-content { + .dynamic-header { + display: flex; + align-items: center; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--el-border-color-light); + + .dynamic-icon { + font-size: 16px; + margin-right: 8px; + } + + .dynamic-type { + font-size: 14px; + font-weight: bold; + margin-right: 8px; + } + + .status-tag { + margin-left: auto; + color: white; + border: none; + } + } + + .dynamic-info { + .goods-info { + display: flex; + align-items: flex-start; + margin-bottom: 8px; + + .goods-image { + width: 120px; + height: 90px; + border-radius: 4px; + margin-right: 12px; + flex-shrink: 0; + cursor: pointer; + border: 1px solid var(--el-border-color-light); + } + + .goods-details { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 60px; + + .goods-name { + font-size: 14px; + font-weight: bold; + color: var(--el-text-color-primary); + margin-bottom: 4px; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .goods-price { + font-size: 14px; + color: var(--el-color-primary); + font-weight: bold; + align-self: flex-start; + margin-bottom: 6px; + } + + .user-info { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + + .user-name, + .user-mobile { + font-size: 12px; + color: var(--el-text-color-secondary); + } + } + + .location-info { + display: flex; + justify-content: space-between; + + .cabinet-name, + .cell-no { + font-size: 12px; + color: var(--el-text-color-secondary); + } + } + } + } + + .user-info, + .location-info { + display: none; + } + + .approval-info { + background-color: var(--el-fill-color-light); + border-radius: 4px; + padding: 12px; + margin-top: 8px; + + .images-row { + display: flex; + gap: 20px; + margin-bottom: 12px; + flex-wrap: wrap; + } + + .images-section { + flex: 1; + min-width: 0; + + .images-label { + font-size: 12px; + font-weight: bold; + color: var(--el-text-color-secondary); + margin-bottom: 6px; + } + + .images-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + + .return-image, + .audit-image { + width: 110px; + height: 70px; + border-radius: 4px; + cursor: pointer; + border: 1px solid var(--el-border-color-light); + flex-shrink: 0; + } + } + } + + .audit-info, + .remark-info { + font-size: 12px; + color: var(--el-text-color-secondary); + + .audit-label, + .remark-label { + font-weight: bold; + } + + .audit-name, + .audit-remark { + color: var(--el-text-color-primary); + } + } + + .remark-info { + margin-top: 4px; + } + } + } + } + } + + .image-error { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--el-fill-color-light); + color: var(--el-text-color-secondary); + font-size: 12px; + border-radius: 4px; + } + + .dynamic-empty { + text-align: center; + padding: 40px 0; + + :deep(.el-empty) { + .el-empty__description { + font-size: 14px; + color: var(--el-text-color-secondary); + } + } + } + } + } } \ No newline at end of file