# 哈哈零售 Java SDK

[![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE)
[![Java](https://img.shields.io/badge/Java-17+-orange.svg)](https://www.oracle.com/java/technologies/downloads/)
[![Maven](https://img.shields.io/badge/Maven-3.6+-red.svg)](https://maven.apache.org/)

哈哈零售系统 Java SDK，为智能视觉识别售卖机系统提供完整的API接口封装。

## 📋 目录

- [功能特性](#功能特性)
- [快速开始](#快速开始)
- [核心功能](#核心功能)
  - [设备管理](#1-设备管理)
  - [商品管理](#2-商品管理)
  - [订单管理](#3-订单管理)
- [高级用法](#高级用法)
- [异常处理](#异常处理)
- [完整接口列表](#完整接口列表)
- [常见问题](#常见问题)

---

## 功能特性

✅ **完整接口覆盖** - 支持哈哈零售平台全部31个API接口  
✅ **自动令牌管理** - 自动获取、刷新和管理access_token（15天有效期）  
✅ **自动签名** - 内置MD5签名算法，无需手动处理  
✅ **类型安全** - 提供完整的类型定义和泛型支持  
✅ **日志支持** - 集成SLF4J日志框架  
✅ **连接池管理** - 基于OkHttp的高性能HTTP客户端  

---

## 快速开始

### 1. Maven依赖

```xml
<dependency>
    <groupId>com.haha</groupId>
    <artifactId>haha-sdk</artifactId>
    <version>1.0.0</version>
</dependency>
```

### 2. 初始化SDK

```java
import com.haha.sdk.HahaClient;
import com.haha.sdk.config.HahaConfig;

// 创建配置
HahaConfig config = HahaConfig.builder()
    .apiBaseUrl("http://api.hahabianli.com")  // API地址（默认值）
    .appId("your_app_id")                      // 商家AppId
    .appSecret("your_app_secret")              // 商家AppSecret
    .connectTimeout(10)                        // 连接超时（秒）
    .readTimeout(30)                           // 读取超时（秒）
    .build();

// 创建客户端
HahaClient client = new HahaClient(config);

// 使用完毕后关闭（可选）
// client.shutdown();
```

### 3. 快速调用示例

```java
import com.haha.sdk.exception.HahaException;

try {
    // 获取设备列表
    List<DeviceInfo> devices = client.getDeviceApi().getDeviceList(1, 20);
    
    // 开门
    OpenDoorResult result = client.getDeviceApi()
        .openDoor("device001", "ORDER" + System.currentTimeMillis(), null);
    System.out.println("订单号: " + result.getOrderNo());
    
} catch (HahaException e) {
    System.err.println("错误: " + e.getMessage());
}
```

---

## 核心功能

### 1. 设备管理

通过 `client.getDeviceApi()` 访问设备相关接口：

#### 1.1 获取设备列表

```java
// 获取第1页，每页20条
List<DeviceInfo> devices = client.getDeviceApi().getDeviceList(1, 20);

for (DeviceInfo device : devices) {
    System.out.println("设备ID: " + device.getId());
    System.out.println("设备名称: " + device.getName());
    System.out.println("设备地址: " + device.getAddress());
    System.out.println("设备状态: " + device.getStatus()); // 1-正常, 2-冻结
}
```

#### 1.2 查询设备是否多门单开

```java
String deviceId = "device001";
Integer multiDoorType = client.getDeviceApi().isMultiDoorUnique(deviceId);

// 返回值：0-非多门单开，2-双门单开，3-三门单开，4-四门单开
if (multiDoorType == 2) {
    System.out.println("这是双门单开设备");
}
```

#### 1.3 设备开门

```java
String deviceId = "device001";
String outTradeNo = "ORDER" + System.currentTimeMillis(); // 商户订单号
Integer doorIndex = null; // 多门单开时必传：0-A门，1-B门

OpenDoorResult result = client.getDeviceApi().openDoor(deviceId, outTradeNo, doorIndex);
System.out.println("哈哈平台订单号: " + result.getOrderNo());
```

#### 1.4 查询设备在线状态

```java
DeviceOnlineStatus status = client.getDeviceApi().getOnlineStatus("device001");

System.out.println("在线状态: " + status.getOnlineStatus()); // 0-离线, 1-在线
System.out.println("最后在线时间: " + status.getLastOnlineTime());
```

#### 1.5 设置设备音量

```java
// 音量范围：0-100，0表示静音
boolean success = client.getDeviceApi().setVolume("device001", 80);
```

#### 1.6 查询设备和锁的状态

```java
Map<String, Object> status = client.getDeviceApi().getDeviceStatus("device001");

System.out.println("设备状态: " + status.get("device_status")); // 0-离线, 1-在线, 2-故障
System.out.println("门状态: " + status.get("door_status"));     // 0-关闭, 1-打开
System.out.println("锁状态: " + status.get("lock_status"));     // 0-未锁, 1-已锁
System.out.println("摄像头: " + status.get("camera_status"));   // 0-异常, 1-正常
System.out.println("网络: " + status.get("network_status"));    // 0-断开, 1-连接
```

---

### 2. 商品管理

通过 `client.getGoodsApi()` 访问商品相关接口：

#### 2.1 获取设备可售卖商品列表

```java
List<Map<String, Object>> goodsList = client.getGoodsApi()
    .getDeviceGoodsList("device001");

for (Map<String, Object> goods : goodsList) {
    System.out.println("商品ID: " + goods.get("goods_id"));
    System.out.println("商品名: " + goods.get("goods_name"));
    System.out.println("价格: " + goods.get("price"));
    System.out.println("库存: " + goods.get("stock"));
}
```

#### 2.2 获取商品总库

```java
// 分页查询，支持关键词搜索
Map<String, Object> result = client.getGoodsApi()
    .getTotalList(1, 20, "可乐");

int total = (int) result.get("total");
List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("list");
```

#### 2.3 添加商品至商家商品库

```java
String goodsId = "goods001";  // 来自商品总库的商品ID
String price = "3.50";        // 售价

boolean success = client.getGoodsApi().addToMerchant(goodsId, price);
```

#### 2.4 获取商家商品库

```java
Map<String, Object> result = client.getGoodsApi().getMerchantList(1, 20);

int total = (int) result.get("total");
List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("list");
```

#### 2.5 商品上下架

```java
String deviceId = "device001";
String goodsId = "goods001";
int status = 1; // 0-下架, 1-上架

boolean success = client.getGoodsApi().setStatus(deviceId, goodsId, status);
```

#### 2.6 获取和更新设备层模板

```java
// 获取层模板
Map<String, Object> template = client.getGoodsApi().getLayerTemplate("device001");

// 更新层模板（layers为JSON格式的层级配置）
String layersJson = "[{\"layer_no\":1,\"layer_name\":\"第一层\",\"goods_list\":[]}]";
boolean success = client.getGoodsApi().updateLayerTemplate("device001", layersJson);
```

#### 2.7 新品申请

```java
boolean success = client.getGoodsApi().applyNew(
    "新商品名称",
    "6901234567890",
    "http://example.com/image.jpg",
    "商品描述（可选）"
);
```

#### 2.8 查询商品编号对应关系

```java
// 通过商家商品ID查询
Map<String, Object> mapping = client.getGoodsApi()
    .getIdMapping("merchant_001", null);

// 或通过哈哈平台商品ID查询
mapping = client.getGoodsApi().getIdMapping(null, "haha_001");
```

#### 2.9 商品合并

```java
String targetGoodsId = "goods001";           // 保留的商品
String sourceGoodsIds = "goods002,goods003"; // 要合并的商品（逗号分隔）

boolean success = client.getGoodsApi().mergeGoods(targetGoodsId, sourceGoodsIds);
```

---

### 3. 订单管理

通过 `client.getOrderApi()` 访问订单相关接口：

#### 3.1 查询识别结果

```java
// 方式1：通过哈哈平台订单号查询
Map<String, Object> result = client.getOrderApi()
    .getRecognizeResult("HH202401240001", null);

// 方式2：通过商户订单号查询
result = client.getOrderApi()
    .getRecognizeResult(null, "ORDER123456");

// 解析结果
List<Map<String, Object>> goodsList = (List) result.get("goods_list");
Double totalAmount = (Double) result.get("total_amount");
Double confidence = (Double) result.get("confidence");
String videoUrl = (String) result.get("video_url");

System.out.println("总金额: " + totalAmount);
System.out.println("置信度: " + confidence);
```

#### 3.2 查询订单详情

```java
Map<String, Object> order = client.getOrderApi()
    .queryOrder("HH202401240001", null);

String orderStatus = (String) order.get("order_status"); // paying, paid, refund, closed
System.out.println("订单状态: " + orderStatus);
```

#### 3.3 设置订单支付状态

```java
String orderNo = "HH202401240001";
int payStatus = 1; // 1-支付成功, 2-支付失败
String payTime = "2024-01-24 15:30:00";
String transactionId = "WX123456789"; // 第三方支付流水号

boolean success = client.getOrderApi()
    .setPayStatus(orderNo, payStatus, payTime, transactionId);
```

#### 3.4 获取开门前后图片或视频

```java
Map<String, Object> media = client.getOrderApi()
    .getOrderMedia("HH202401240001");

String beforeImage = (String) media.get("before_image");
String afterImage = (String) media.get("after_image");
String videoUrl = (String) media.get("video_url");
```

#### 3.5 查询回调状态

```java
Map<String, Object> callbackStatus = client.getOrderApi()
    .getCallbackStatus("HH202401240001");

int status = (int) callbackStatus.get("callback_status"); // 0-未推送, 1-成功, 2-失败
int retryCount = (int) callbackStatus.get("retry_count");
```

#### 3.6 获取静态柜分层识别结果

```java
Map<String, Object> layerResult = client.getOrderApi()
    .getLayerRecognize("HH202401240001");

List<Map<String, Object>> layers = (List) layerResult.get("layers");
for (Map<String, Object> layer : layers) {
    int layerNo = (int) layer.get("layer_no");
    String beforeImage = (String) layer.get("before_image");
    String afterImage = (String) layer.get("after_image");
    List<Map<String, Object>> goodsList = (List) layer.get("goods_list");
}
```

---

## 高级用法

### 手动管理access_token

SDK会自动管理token，但如果需要手动设置：

```java
// 手动设置token（如果你已经有token并自行管理）
client.setAccessToken("your_access_token", 1296000); // 15天有效期（秒）

// 或者让SDK自动获取token
String token = client.getAccessToken();
```

### 自定义HTTP配置

```java
HahaConfig config = HahaConfig.builder()
    .apiBaseUrl("http://api.hahabianli.com")
    .appId("your_app_id")
    .appSecret("your_app_secret")
    .connectTimeout(15)    // 连接超时15秒
    .readTimeout(60)       // 读取超时60秒
    .writeTimeout(60)      // 写入超时60秒
    .enableLog(true)       // 启用日志
    .build();

HahaClient client = new HahaClient(config);
```

### 资源释放

```java
// 程序退出前关闭客户端，释放连接池资源
client.shutdown();
```

---

## 异常处理

SDK统一使用 `HahaException` 异常：

```java
import com.haha.sdk.exception.HahaException;

try {
    OpenDoorResult result = client.getDeviceApi()
        .openDoor("device001", "ORDER001", null);
    
} catch (HahaException e) {
    // 错误码（对应API文档的错误码）
    Integer errorCode = e.getCode();
    
    // 错误信息
    String errorMsg = e.getMessage();
    
    System.err.println("错误码: " + errorCode + ", 错误信息: " + errorMsg);
    
    // 如果需要查看底层异常
    if (e.getCause() != null) {
        e.getCause().printStackTrace();
    }
}
```

### 常见错误码

| 错误码 | 说明 | 处理方案 |
|--------|------|----------|
| 1 | SUCCESS | 操作成功 |
| 0 | FAIL | 操作失败 |
| -1 | 参数错误 | 检查请求参数 |
| -2 | 签名错误 | 检查AppSecret |
| -3 | token无效 | 重新获取token |
| -4 | token过期 | SDK会自动刷新 |
| -100 | 设备不存在 | 检查设备ID |
| -101 | 设备离线 | 等待设备上线 |
| -103 | 设备使用中 | 等待购物结束 |

---

## 完整接口列表

### 设备接口（9个）

| 方法 | 说明 |
|------|------|
| `getDeviceList` | 获取设备列表 |
| `isMultiDoorUnique` | 查询是否多门单开 |
| `openDoor` | 设备开门 |
| `getOnlineStatus` | 查询在线状态 |
| `setVolume` | 设置音量 |
| `getDeviceStatus` | 查询设备和锁状态 |

### 商品接口（10个）

| 方法 | 说明 |
|------|------|
| `getDeviceGoodsList` | 设备可售商品列表 |
| `getTotalList` | 商品总库列表 |
| `addToMerchant` | 添加至商家库 |
| `getMerchantList` | 商家商品库列表 |
| `setStatus` | 商品上下架 |
| `getLayerTemplate` | 获取层模板 |
| `updateLayerTemplate` | 更新层模板 |
| `applyNew` | 新品申请 |
| `getIdMapping` | 商品ID对应关系 |
| `mergeGoods` | 商品合并 |

### 订单接口（8个）

| 方法 | 说明 |
|------|------|
| `getRecognizeResult` | 查询识别结果 |
| `queryOrder` | 查询订单详情 |
| `setPayStatus` | 设置支付状态 |
| `getOrderMedia` | 获取图片/视频 |
| `getCallbackStatus` | 查询回调状态 |
| `getLayerRecognize` | 静态柜分层结果 |

---

## 常见问题

### Q1: access_token有效期多久？

A: 15天（1296000秒）。SDK会自动管理，到期前2天内会自动刷新。

### Q2: 如何处理识别结果通知？

A: 哈哈平台会通过回调地址推送识别结果。商户需要：
1. 在商家后台配置回调地址
2. 接收POST通知
3. 根据商品列表扣款
4. 调用`setPayStatus`接口告知支付结果

### Q3: 识别置信度低于0.8怎么办？

A: 建议通过`getOrderMedia`接口获取购物视频进行人工审核。

### Q4: 多门柜如何开门？

A: 先调用`isMultiDoorUnique`判断设备类型，如果返回2（双门），开门时需传递`doorIndex`参数（0或1）。

### Q5: SDK是否线程安全？

A: 是的。`HahaClient`可以在多线程环境中安全使用，建议作为单例管理。

---

## 技术支持

- 文档地址: https://showdoc.hahabianli.com/web/#/685869388/275897889
- API基础地址: http://api.hahabianli.com
- 联系方式: 请联系哈哈运营人员

---

## 开源协议

Apache License 2.0

---

**版本**: 1.0.0  
**更新时间**: 2024-01-24
