# 35. 提现记录详情查询接口

## 接口基本信息

| 项目 | 内容 |
|------|------|
| 接口名称 | 提现记录详情查询 |
| 接口路径 | `/incomeManage/getCashOutRecordDetail` |
| 请求方式 | GET |
| 接口说明 | 查询指定提现记录的详细信息 |
| 是否需要登录 | 是 |
| 开发者 | ssk |
| 开发日期 | 2025-11-25 |

---

## 业务说明

### 1. 功能描述

该接口用于查询单条提现记录的详细信息，包括提现金额、手续费、提现状态、收益时间范围、到账时间、审批信息等完整数据。

### 2. 使用场景

- 商户查看提现申请的详细信息
- 查看提现审批结果和备注
- 查看提现失败原因
- 查看提现到账时间和支付宝订单号

### 3. 业务规则

1. **权限控制**：只能查看自己店铺的提现记录
2. **数据验证**：提现记录必须存在且未被删除
3. **软删除过滤**：已删除的记录无法查询

### 4. 与列表接口的关系

- **列表接口** (`/incomeManage/getCashOutRecordList`): 分页查询多条提现记录，返回简要信息和统计数据
- **详情接口** (`/incomeManage/getCashOutRecordDetail`): 查询单条记录的完整详细信息

---

## 请求参数

### Query参数

| 参数名 | 类型 | 必填 | 说明 | 示例 |
|--------|------|------|------|------|
| id | Integer | 是 | 提现记录ID | 1001 |

### Headers

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| Authorization | String | 是 | JWT Token（格式：`Bearer {token}`） |

### 说明

- **门店ID**：通过 `LoginUserUtil` 从当前登录用户 Token 中自动获取，用于权限校验
- **权限校验**：自动验证提现记录是否属于当前登录用户的店铺

---

## 响应参数

### 响应格式

```json
{
  "code": 200,
  "success": true,
  "message": "操作成功",
  "data": {
    "id": 1001,
    "storeId": 103,
    "money": 10000,
    "commission": 500,
    "cashOutType": 0,
    "orderNo": "TX202511250001",
    "aliOrderNo": "2025112522001234567890123456",
    "incomeStartTime": "2025-10-28 00:00:00",
    "incomeEndTime": "2025-11-24 23:59:59",
    "paymentDate": "2025-11-25 15:30:00",
    "paymentStatus": 4,
    "alertStatus": 1,
    "deleteFlag": 0,
    "createdTime": "2025-11-25 10:00:00",
    "createdUserId": 113,
    "updatedTime": "2025-11-25 15:30:00",
    "updatedUserId": 1,
    "approveTime": "2025-11-25 11:00:00",
    "payDate": "2025-11-25 15:30:00",
    "failReason": null,
    "storeUserId": 113,
    "comments": "审核通过",
    "settlementAccount": "alipay@example.com",
    "approveFailReason": null
  }
}
```

### 响应字段说明

| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | Integer | 提现记录ID |
| storeId | Integer | 门店ID |
| money | Integer | 提现金额（单位：分） |
| commission | Integer | 手续费（单位：分） |
| cashOutType | Integer | 提现类型（0-手动，1-自动） |
| orderNo | String | 商户订单号 |
| aliOrderNo | String | 支付宝订单号 |
| incomeStartTime | Date | 收益开始时间 |
| incomeEndTime | Date | 收益结束时间 |
| paymentDate | Date | 到账时间 |
| paymentStatus | Integer | 提现状态<br/>0-进行中<br/>1-待审核<br/>2-审核不通过<br/>3-已通过<br/>4-已打款<br/>5-打款失败 |
| alertStatus | Integer | 已到账期弹窗（0-弹，1-不弹） |
| deleteFlag | Integer | 删除标记（0-未删除，1-已删除） |
| createdTime | Date | 创建时间（申请时间） |
| createdUserId | Integer | 创建人ID（申请人） |
| updatedTime | Date | 修改时间 |
| updatedUserId | Integer | 修改人ID |
| approveTime | Date | 审批时间 |
| payDate | Date | 支付时间 |
| failReason | String | 失败原因（打款失败时） |
| storeUserId | Integer | 申请人ID（store_user表） |
| comments | String | 审批备注 |
| settlementAccount | String | 结算账户（支付宝账号） |
| approveFailReason | String | 拒绝原因（审核不通过时） |

---

## 完整示例

### 请求示例

**cURL 示例**

```bash
curl -X GET 'http://localhost:8081/incomeManage/getCashOutRecordDetail?id=1001' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
```

**JavaScript (Axios) 示例**

```javascript
async function getCashOutRecordDetail(id) {
    try {
        const response = await axios.get(
            `/incomeManage/getCashOutRecordDetail?id=${id}`,
            {
                headers: {
                    'Authorization': 'Bearer ' + token
                }
            }
        );
        
        console.log('提现记录详情:', response.data);
        
        if (response.data.success) {
            const record = response.data.data;
            console.log(`提现金额: ${record.money / 100}元`);
            console.log(`提现状态: ${getStatusText(record.paymentStatus)}`);
            console.log(`结算账户: ${record.settlementAccount}`);
        }
    } catch (error) {
        console.error('查询失败:', error.message);
    }
}

function getStatusText(status) {
    const statusMap = {
        0: '进行中',
        1: '待审核',
        2: '审核不通过',
        3: '已通过',
        4: '已打款',
        5: '打款失败'
    };
    return statusMap[status] || '未知状态';
}

// 调用示例
getCashOutRecordDetail(1001);
```

**Java (RestTemplate) 示例**

```java
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8081/incomeManage/getCashOutRecordDetail?id=1001";

// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);

HttpEntity<Void> requestEntity = new HttpEntity<>(headers);

// 发送请求
ResponseEntity<R> response = restTemplate.exchange(
    url,
    HttpMethod.GET,
    requestEntity,
    new ParameterizedTypeReference<R<StoreCashOutRecord>>() {}
);

// 处理响应
R<StoreCashOutRecord> result = response.getBody();
if (result.getSuccess()) {
    StoreCashOutRecord record = result.getData();
    System.out.println("提现金额: " + record.getMoney() / 100.0 + "元");
    System.out.println("提现状态: " + record.getPaymentStatus());
    System.out.println("结算账户: " + record.getSettlementAccount());
}
```

---

### 响应示例

#### 成功响应 - 已打款

```json
{
  "code": 200,
  "success": true,
  "message": "操作成功",
  "data": {
    "id": 1001,
    "storeId": 103,
    "money": 10000,
    "commission": 500,
    "cashOutType": 0,
    "orderNo": "TX202511250001",
    "aliOrderNo": "2025112522001234567890123456",
    "incomeStartTime": "2025-10-28 00:00:00",
    "incomeEndTime": "2025-11-24 23:59:59",
    "paymentDate": "2025-11-25 15:30:00",
    "paymentStatus": 4,
    "alertStatus": 1,
    "deleteFlag": 0,
    "createdTime": "2025-11-25 10:00:00",
    "createdUserId": 113,
    "updatedTime": "2025-11-25 15:30:00",
    "updatedUserId": 1,
    "approveTime": "2025-11-25 11:00:00",
    "payDate": "2025-11-25 15:30:00",
    "failReason": null,
    "storeUserId": 113,
    "comments": "审核通过",
    "settlementAccount": "alipay@example.com",
    "approveFailReason": null
  }
}
```

#### 成功响应 - 审核不通过

```json
{
  "code": 200,
  "success": true,
  "message": "操作成功",
  "data": {
    "id": 1002,
    "storeId": 103,
    "money": 5000,
    "commission": 250,
    "cashOutType": 0,
    "orderNo": "TX202511250002",
    "aliOrderNo": null,
    "incomeStartTime": "2025-10-28 00:00:00",
    "incomeEndTime": "2025-11-24 23:59:59",
    "paymentDate": null,
    "paymentStatus": 2,
    "alertStatus": 0,
    "deleteFlag": 0,
    "createdTime": "2025-11-25 12:00:00",
    "createdUserId": 113,
    "updatedTime": "2025-11-25 13:00:00",
    "updatedUserId": 1,
    "approveTime": "2025-11-25 13:00:00",
    "payDate": null,
    "failReason": null,
    "storeUserId": 113,
    "comments": null,
    "settlementAccount": "alipay@example.com",
    "approveFailReason": "账户信息有误，请核实后重新申请"
  }
}
```

#### 失败响应 - 记录不存在

```json
{
  "code": 500,
  "success": false,
  "message": "查询失败：提现记录不存在",
  "data": null
}
```

#### 失败响应 - 无权查看

```json
{
  "code": 500,
  "success": false,
  "message": "查询失败：无权查看该提现记录",
  "data": null
}
```

#### 失败响应 - 参数错误

```json
{
  "code": 500,
  "success": false,
  "message": "查询失败：提现记录ID不能为空",
  "data": null
}
```

---

## 业务流程

```
┌─────────────┐
│   前端发起   │
│   详情查询   │
└──────┬──────┘
       │
       ▼
┌─────────────────┐
│  提取 Token     │
│  获取 storeId   │
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│  参数验证        │
│  - id 非空      │
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│  查询提现记录    │
│  根据 id 查询   │
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│  记录存在性检查  │
│  - 记录不存在   │
│  - 已被删除     │
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│  权限校验        │
│  对比 storeId   │
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│  返回记录详情    │
└─────────────────┘
```

---

## 核心代码实现

### 1. Controller 层

**文件路径**: `alien-store-platform/src/main/java/shop/alien/storeplatform/controller/IncomeManageController.java`

```java
@ApiOperation("提现记录详情查询")
@ApiOperationSupport(order = 5)
@ApiImplicitParams({
        @ApiImplicitParam(name = "id", value = "提现记录ID", dataType = "Integer", paramType = "query", required = true)
})
@GetMapping("/getCashOutRecordDetail")
public R<StoreCashOutRecord> getCashOutRecordDetail(@RequestParam("id") Integer id) {
    log.info("IncomeManageController.getCashOutRecordDetail?id={}", id);
    try {
        StoreCashOutRecord result = incomeManageService.getCashOutRecordDetail(id);
        return R.data(result);
    } catch (Exception e) {
        log.error("IncomeManageController.getCashOutRecordDetail ERROR: {}", e.getMessage(), e);
        return R.fail("查询失败：" + e.getMessage());
    }
}
```

---

### 2. Service 接口层

**文件路径**: `alien-store-platform/src/main/java/shop/alien/storeplatform/service/IncomeManageService.java`

```java
/**
 * 提现记录详情查询
 *
 * @param id 提现记录ID
 * @return 提现记录详情
 */
StoreCashOutRecord getCashOutRecordDetail(Integer id);
```

---

### 3. Service 实现层

**文件路径**: `alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/IncomeManageServiceImpl.java`

```java
/**
 * 提现记录详情查询
 *
 * @param id 提现记录ID
 * @return 提现记录详情
 */
@Override
public StoreCashOutRecord getCashOutRecordDetail(Integer id) {
    log.info("IncomeManageServiceImpl.getCashOutRecordDetail - 开始查询提现记录详情: id={}", id);

    // ==================== 1. 参数验证 ====================
    if (id == null) {
        log.warn("IncomeManageServiceImpl.getCashOutRecordDetail - 提现记录ID为空");
        throw new RuntimeException("提现记录ID不能为空");
    }

    // ==================== 2. 查询提现记录 ====================
    StoreCashOutRecord record = storeCashOutRecordMapper.selectById(id);
    if (record == null || record.getDeleteFlag() == 1) {
        log.warn("IncomeManageServiceImpl.getCashOutRecordDetail - 提现记录不存在: id={}", id);
        throw new RuntimeException("提现记录不存在");
    }

    // ==================== 3. 权限校验（只能查看自己店铺的提现记录） ====================
    Integer storeId = LoginUserUtil.getCurrentStoreId();
    if (!record.getStoreId().equals(storeId)) {
        log.warn("IncomeManageServiceImpl.getCashOutRecordDetail - 无权查看他人提现记录: currentStoreId={}, recordStoreId={}",
                storeId, record.getStoreId());
        throw new RuntimeException("无权查看该提现记录");
    }

    log.info("IncomeManageServiceImpl.getCashOutRecordDetail - 查询完成: id={}, storeId={}, money={}, status={}",
            id, record.getStoreId(), record.getMoney(), record.getPaymentStatus());

    return record;
}
```

---

## 技术要点

### 1. 权限控制

```java
// 从 Token 中获取当前登录用户的店铺ID
Integer storeId = LoginUserUtil.getCurrentStoreId();

// 对比提现记录的店铺ID
if (!record.getStoreId().equals(storeId)) {
    throw new RuntimeException("无权查看该提现记录");
}
```

**防止越权访问**:
- 用户只能查看自己店铺的提现记录
- storeId 从 Token 中获取，前端无法伪造

---

### 2. 数据验证

```java
// 验证记录存在性和软删除状态
if (record == null || record.getDeleteFlag() == 1) {
    throw new RuntimeException("提现记录不存在");
}
```

---

### 3. 提现状态说明

| 状态值 | 状态名称 | 说明 |
|-------|---------|------|
| 0 | 进行中 | 提现正在处理中 |
| 1 | 待审核 | 等待管理员审核 |
| 2 | 审核不通过 | 审核被拒绝，查看 `approveFailReason` |
| 3 | 已通过 | 审核通过，等待打款 |
| 4 | 已打款 | 提现成功，已打款到支付宝 |
| 5 | 打款失败 | 打款失败，查看 `failReason` |

---

### 4. 金额单位转换

数据库存储的金额单位是**分**，前端展示时需要转换为**元**：

```javascript
// 转换为元（保留两位小数）
const moneyInYuan = record.money / 100;
const commissionInYuan = record.commission / 100;
const actualAmount = (record.money - record.commission) / 100;

console.log(`提现金额: ${moneyInYuan}元`);
console.log(`手续费: ${commissionInYuan}元`);
console.log(`实际到账: ${actualAmount}元`);
```

---

## 与列表接口的配合使用

### 典型使用流程

```
┌──────────────────┐
│  1. 查询列表      │
│  getCashOutRecordList │
│  (分页、筛选)     │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  2. 选择记录      │
│  用户点击某条记录  │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  3. 查询详情      │
│  getCashOutRecordDetail │
│  (完整信息)       │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  4. 展示详情页    │
│  显示所有字段信息  │
└──────────────────┘
```

### 前端联动示例

```javascript
// 步骤1: 查询列表
async function loadCashOutList() {
    const response = await axios.get('/incomeManage/getCashOutRecordList', {
        params: {
            page: 1,
            size: 10
        }
    });
    
    // 渲染列表，每条记录有"查看详情"按钮
    renderList(response.data.data.cashOutRecordList);
}

// 步骤2: 点击查看详情
function viewDetail(recordId) {
    getCashOutRecordDetail(recordId);
}

// 步骤3: 加载详情
async function getCashOutRecordDetail(id) {
    const response = await axios.get('/incomeManage/getCashOutRecordDetail', {
        params: { id }
    });
    
    // 展示详情弹窗或跳转详情页
    showDetailModal(response.data.data);
}
```

---

## 测试建议

### 1. 单元测试

```java
@Test
public void testGetCashOutRecordDetail_Success() {
    // 准备测试数据
    Integer recordId = 1001;
    
    // 执行查询
    StoreCashOutRecord result = incomeManageService.getCashOutRecordDetail(recordId);
    
    // 验证结果
    assertNotNull(result);
    assertEquals(recordId, result.getId());
    assertEquals(103, result.getStoreId().intValue());
}

@Test
public void testGetCashOutRecordDetail_NotFound() {
    // 查询不存在的记录
    assertThrows(RuntimeException.class, () -> {
        incomeManageService.getCashOutRecordDetail(99999);
    });
}

@Test
public void testGetCashOutRecordDetail_NoPermission() {
    // 模拟查询其他店铺的记录
    // ... 验证抛出权限异常
}
```

---

### 2. 接口测试（Postman）

**测试场景1: 正常查询**
1. 先调用列表接口获取记录ID
2. 使用该ID调用详情接口
3. 验证返回完整数据

**测试场景2: 记录不存在**
1. 使用不存在的ID（如99999）
2. 验证返回错误提示

**测试场景3: 权限校验**
1. 使用用户A的Token
2. 查询用户B的提现记录ID
3. 验证返回"无权查看"错误

---

## 常见问题（FAQ）

### Q1: 详情接口和列表接口有什么区别？

**A**: 
- **列表接口**: 分页查询多条记录，返回简要信息 + 统计数据
- **详情接口**: 查询单条记录的**所有字段**，包括审批备注、失败原因等

### Q2: 为什么不在列表接口直接返回所有详情？

**A**: 
- 性能优化：列表只返回必要字段，减少数据传输
- 按需加载：只有用户点击查看时才加载完整详情
- 分离关注点：列表关注概览，详情关注完整信息

### Q3: 能否查看其他店铺的提现记录？

**A**: 不能。系统会自动验证提现记录的 `storeId` 是否与当前登录用户的店铺ID一致，防止越权访问。

### Q4: 已删除的记录能否查询？

**A**: 不能。软删除（`deleteFlag=1`）的记录会被过滤，无法通过详情接口查询。

---

## 相关文档

- [24. 提现记录查询接口（列表）](./24-提现记录查询接口.md)
- [23. 申请提现接口](./23-申请提现接口.md)
- [28. 快速提现申请接口](./28-快速提现申请接口.md)

---

## 版本历史

| 版本号 | 日期 | 修改内容 | 修改人 |
|-------|------|---------|--------|
| v1.0 | 2025-11-25 | 初始版本，新增提现记录详情查询接口 | ssk |

---

## 总结

提现记录详情查询接口提供了单条提现记录的完整信息查询功能，配合列表接口使用，为商户提供了完整的提现记录管理体验。接口包含完善的权限控制和数据验证，确保数据安全性。

