fujian_water_biz_doc/water_biz_interface_design.md

1335 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 福建水务营收系统接口设计文档
## 文档信息
| 项目信息 | 详情 |
|---------|------|
| **项目名称** | 福建水务营收系统 |
| **文档类型** | 概要设计文档 |
| **技术框架** | RuoYi-Vue-Pro + yudao-ui-admin-vue3 |
| **文档版本** | v1.0 |
| **编写日期** | 2024-12-19 |
| **文档状态** | 🟡 进行中 |
## 目录
- [1. 接口概述](#1-接口概述)
- [2. 外部接口](#2-外部接口)
- [3. 内部接口](#3-内部接口)
- [4. 接口标准](#4-接口标准)
## 1. 接口概述
福建水务业务系统提供丰富的接口用于与外部系统集成以及系统内部各模块间的数据交换。接口设计遵循标准化、安全性、可扩展性的原则基于RuoYi-Vue-Pro框架采用RESTful风格设计支持JSON数据格式。
### 1.1 接口设计原则
- **统一性**:所有接口遵循统一的设计规范和数据格式
- **安全性**:接口通过认证授权、参数校验等机制保障安全
- **可维护性**:接口文档完善,便于维护和升级
- **兼容性**:接口设计考虑版本兼容,支持平滑升级
- **性能优化**:接口设计考虑性能,支持缓存、分页等机制
### 1.2 RESTful API规范
系统API接口采用RESTful风格设计主要规范如下
#### 1.2.1 资源命名
- 使用名词复数表示资源集合,如`/users``/meters`
- 使用资源ID标识特定资源`/users/1``/meters/123`
- 资源层级关系通过路径嵌套表示,如`/users/1/meters`
#### 1.2.2 HTTP方法
- GET获取资源
- POST创建资源
- PUT更新资源全量更新
- PATCH部分更新资源
- DELETE删除资源
#### 1.2.3 状态码
- 200 OK请求成功
- 201 Created资源创建成功
- 400 Bad Request请求参数错误
- 401 Unauthorized未授权
- 403 Forbidden权限不足
- 404 Not Found资源不存在
- 500 Internal Server Error服务器内部错误
#### 1.2.4 响应格式
系统统一采用以下JSON格式响应
```json
{
"code": 0, // 业务状态码0表示成功非0表示失败
"data": {}, // 响应数据
"msg": "success" // 响应消息
}
```
分页查询响应格式:
```json
{
"code": 0,
"data": {
"list": [], // 数据列表
"total": 100, // 总记录数
"pageNum": 1, // 当前页码
"pageSize": 10 // 每页记录数
},
"msg": "success"
}
```
### 1.3 接口文档
系统使用Knife4j基于Swagger自动生成API文档文档地址为`http://{系统地址}/doc.html`
主要特点:
- 在线接口文档:支持在线查看接口定义
- 接口调试:支持在线调试接口
- 文档导出支持导出OpenAPI规范文档
- 权限控制:支持对接口文档的访问控制
## 2. 外部接口
### 2.1 银行接口对接
#### 2.1.1 银行代扣接口
**功能描述**:通过银行系统自动从用户账户中扣除水费。
**接口详情**
- **接口方式**文件交换FTP/SFTP
- **数据格式**:定长文本文件
- **交换频率**每日凌晨2:00
- **文件编码**GBK
**代扣文件格式**
```text
记录类型(1位) + 客户号(12位) + 户名(30位) + 银行账号(20位) + 扣款金额(12位,含2位小数) + 账期(6位) + 保留字段(19位)
```
**代扣文件示例**
```text
1C00000000001张三 62172511001234567890000009180202412
1C00000000002李四 62172511001234567891000015460202412
```
**回盘文件格式**
```text
记录类型(1位) + 客户号(12位) + 银行账号(20位) + 扣款金额(12位) + 处理状态(1位) + 银行流水号(20位) + 处理时间(14位) + 失败原因(20位)
```
**Java实现示例**
```java
@Service
public class BankDeductServiceImpl implements BankDeductService {
@Resource
private SftpTemplate sftpTemplate;
@Resource
private BillService billService;
@Scheduled(cron = "0 0 2 * * ?")
public void generateDeductFile() {
LocalDate deductDate = LocalDate.now();
// 获取待代扣账单
List<BillDO> deductBills = billService.getDeductBills(deductDate);
// 生成代扣文件
String fileName = "DEDUCT_" + deductDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".txt";
String fileContent = buildDeductFileContent(deductBills);
// 上传至银行SFTP
sftpTemplate.put(fileName, fileContent.getBytes(StandardCharsets.UTF_8), "/upload/");
// 记录代扣文件日志
DeductFileLogDO log = new DeductFileLogDO();
log.setFileName(fileName);
log.setFileStatus("UPLOADED");
log.setRecordCount(deductBills.size());
deductFileLogMapper.insert(log);
}
private String buildDeductFileContent(List<BillDO> bills) {
StringBuilder content = new StringBuilder();
for (BillDO bill : bills) {
content.append("1") // 记录类型
.append(StringUtils.rightPad(bill.getCustomerCode(), 12)) // 客户号
.append(StringUtils.rightPad(bill.getCustomerName(), 30)) // 户名
.append(StringUtils.rightPad(bill.getBankAccount(), 20)) // 银行账号
.append(String.format("%012d", bill.getTotalAmount().multiply(new BigDecimal(100)).intValue())) // 金额(分)
.append(bill.getBillMonth().replace("-", "")) // 账期
.append(StringUtils.repeat(" ", 19)) // 保留字段
.append("\n");
}
return content.toString();
}
}
```
#### 2.1.2 银行实时缴费接口
**功能描述**:用户在银行柜台、网上银行或手机银行实时缴纳水费。
**接口详情**
- **接口方式**HTTP POST
- **请求URL**`https://bank.api.com/payment/water-fee`
- **数据格式**JSON
- **认证方式**API Key + 签名
**请求参数**
```json
{
"merchantId": "WATER001",
"customerCode": "C001",
"billCodes": ["B202412190001"],
"totalAmount": 91.80,
"bankAccount": "6217251100123456789",
"customerName": "张三",
"timestamp": "20241219103000",
"signature": "ABC123DEF456..."
}
```
**响应参数**
```json
{
"resultCode": "0000",
"resultMsg": "交易成功",
"data": {
"transactionId": "TXN20241219001",
"paymentTime": "20241219103001",
"bankSerial": "BNK20241219001234"
}
}
```
### 2.2 支付宝接口对接
**功能描述**用户通过支付宝缴纳水费支持扫码支付和H5支付。
**接口详情**
- **接口方式**HTTP POST
- **支付方式**统一收单交易预创建alipay.trade.precreate
- **数据格式**JSON
- **认证方式**RSA2签名
**预创建支付请求参数**
```json
{
"app_id": "2021001234567890",
"method": "alipay.trade.precreate",
"charset": "UTF-8",
"sign_type": "RSA2",
"timestamp": "2024-12-19 10:30:00",
"version": "1.0",
"notify_url": "https://water.example.com/api/payment/alipay/notify",
"biz_content": {
"out_trade_no": "P202412190002",
"total_amount": "91.80",
"subject": "水费缴费",
"body": "2024年12月水费-客户编号:C001",
"store_id": "WATER_STORE_001",
"timeout_express": "30m"
}
}
```
**支付宝响应参数**
```json
{
"alipay_trade_precreate_response": {
"code": "10000",
"msg": "Success",
"out_trade_no": "P202412190002",
"qr_code": "https://qr.alipay.com/bax08945xtdnfwgqmwi200b4"
},
"sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
}
```
**Java实现示例**
```java
@Service
public class AlipayServiceImpl implements AlipayService {
@Resource
private AlipayClient alipayClient;
@Override
public AlipayPaymentRespVO createPayment(AlipayPaymentReqVO request) {
AlipayTradePrecreateRequest alipayRequest = new AlipayTradePrecreateRequest();
alipayRequest.setNotifyUrl("https://water.example.com/api/payment/alipay/notify");
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setOutTradeNo(request.getPaymentCode());
model.setTotalAmount(request.getTotalAmount().toString());
model.setSubject("水费缴费");
model.setBody("账单号:" + String.join(",", request.getBillCodes()));
model.setTimeoutExpress("30m");
alipayRequest.setBizModel(model);
try {
AlipayTradePrecreateResponse response = alipayClient.execute(alipayRequest);
if (response.isSuccess()) {
return AlipayPaymentRespVO.builder()
.paymentCode(request.getPaymentCode())
.qrCode(response.getQrCode())
.outTradeNo(response.getOutTradeNo())
.build();
} else {
throw new BizException(ALIPAY_PAY_FAILED, response.getSubMsg());
}
} catch (AlipayApiException e) {
throw new BizException(ALIPAY_PAY_ERROR, e.getErrMsg());
}
}
}
```
### 2.3 微信支付接口对接
**功能描述**:用户通过微信支付缴纳水费,支持扫码支付和小程序支付。
**接口详情**
- **接口方式**HTTP POST
- **支付方式**Native支付扫码/ JSAPI支付小程序
- **请求URL**`https://api.mch.weixin.qq.com/v3/pay/transactions/native`
- **数据格式**JSON
- **认证方式**微信支付V3签名
**统一下单请求参数**
```json
{
"appid": "wx8888888888888888",
"mchid": "1900000109",
"description": "水费缴费-2024年12月",
"out_trade_no": "P202412190003",
"notify_url": "https://water.example.com/api/payment/wechat/notify",
"amount": {
"total": 9180,
"currency": "CNY"
},
"attach": "客户编号:C001,账单号:B202412190001",
"goods_tag": "WATER_FEE",
"time_expire": "2024-12-19T11:00:00+08:00"
}
```
**微信支付响应参数**
```json
{
"code_url": "weixin://wxpay/bizpayurl?pr=HuaLcAKwa"
}
```
**支付结果通知参数**
```json
{
"id": "EV-2018022511223320873",
"create_time": "2024-12-19T10:30:00+08:00",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"summary": "支付成功",
"resource": {
"original_type": "transaction",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "...",
"associated_data": "transaction",
"nonce": "..."
}
}
```
### 2.4 短信接口
**功能描述**:向用户发送各类业务通知短信。
**接口规范**
- 接口方式HTTP接口
- 数据格式JSON
- 交换频率:实时
### 2.5 物联网集抄平台接口
**功能描述**:与物联网集抄平台交互,获取智能水表数据。
**接口规范**
- 接口方式HTTP接口或WebService
- 数据格式JSON或XML
- 交换频率:定时或实时
## 3. 内部接口
### 3.1 客户管理API接口
#### 3.1.1 客户信息查询接口
**功能描述**根据客户ID查询客户详细信息。
**接口详情**
- **请求方式**GET
- **请求路径**`/admin-api/water/customer/{id}`
- **请求头**`Authorization: Bearer {token}`
**请求参数**
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| id | Long | 是 | 客户ID | 1 |
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"customerCode": "C001",
"customerName": "张三",
"customerType": "RESIDENT",
"phone": "13800138000",
"address": "福建省福州市台江区XX街道XX号",
"status": 1,
"createTime": "2024-12-19 10:00:00"
}
}
```
**RuoYi-Vue-Pro代码示例**
```java
@RestController
@RequestMapping("/admin-api/water/customer")
@Tag(name = "管理后台 - 客户管理")
@Validated
public class CustomerController {
@Resource
private CustomerService customerService;
@GetMapping("/{id}")
@Operation(summary = "获得客户")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('water:customer:query')")
public CommonResult<CustomerRespVO> getCustomer(@PathVariable("id") Long id) {
CustomerDO customer = customerService.getCustomer(id);
return success(BeanUtils.toBean(customer, CustomerRespVO.class));
}
}
```
#### 3.1.2 客户分页查询接口
**功能描述**:分页查询客户列表信息。
**接口详情**
- **请求方式**GET
- **请求路径**`/admin-api/water/customer/page`
**请求参数**
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| pageNo | Integer | 否 | 页码默认1 | 1 |
| pageSize | Integer | 否 | 每页条数默认10 | 10 |
| customerName | String | 否 | 客户名称 | 张三 |
| customerCode | String | 否 | 客户编号 | C001 |
| customerType | String | 否 | 客户类型 | RESIDENT |
| phone | String | 否 | 联系电话 | 138 |
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"list": [
{
"id": 1,
"customerCode": "C001",
"customerName": "张三",
"customerType": "RESIDENT",
"phone": "13800138000",
"address": "福建省福州市台江区XX街道XX号",
"status": 1,
"createTime": "2024-12-19 10:00:00"
}
],
"total": 1
}
}
```
#### 3.1.3 客户创建接口
**功能描述**:创建新客户记录。
**接口详情**
- **请求方式**POST
- **请求路径**`/admin-api/water/customer/create`
**请求参数**
```json
{
"customerCode": "C002",
"customerName": "李四",
"customerType": "RESIDENT",
"idType": "ID_CARD",
"idNumber": "350103199001011234",
"phone": "13900139000",
"address": "福建省福州市鼓楼区XX街道XX号"
}
```
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": 2
}
```
**Service层代码示例**
```java
@Service
@Validated
public class CustomerServiceImpl implements CustomerService {
@Resource
private CustomerMapper customerMapper;
@Override
public Long createCustomer(CustomerSaveReqVO createReqVO) {
// 校验客户编号唯一性
validateCustomerCodeUnique(createReqVO.getCustomerCode());
// 创建客户
CustomerDO customer = BeanUtils.toBean(createReqVO, CustomerDO.class);
customerMapper.insert(customer);
return customer.getId();
}
private void validateCustomerCodeUnique(String customerCode) {
CustomerDO existCustomer = customerMapper.selectByCustomerCode(customerCode);
if (existCustomer != null) {
throw exception(CUSTOMER_CODE_DUPLICATE);
}
}
}
```
### 3.2 水表管理API接口
#### 3.2.1 水表信息查询接口
**功能描述**根据水表ID查询水表详细信息。
**接口详情**
- **请求方式**GET
- **请求路径**`/admin-api/water/meter/{id}`
- **请求头**`Authorization: Bearer {token}`
**请求参数**
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| id | Long | 是 | 水表ID | 1 |
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"meterCode": "M001",
"meterNo": "20241219001",
"meterType": "SMART",
"meterModel": "LXSY-15E",
"meterCaliber": "15mm",
"installDate": "2024-01-15",
"installPosition": "1层水表井",
"initialReading": 0.00,
"currentReading": 156.32,
"readingCycle": "MONTHLY",
"meterStatus": 1,
"customerId": 1,
"customerName": "张三"
}
}
```
**Controller代码示例**
```java
@RestController
@RequestMapping("/admin-api/water/meter")
@Tag(name = "管理后台 - 水表管理")
@Validated
public class MeterController {
@Resource
private MeterService meterService;
@GetMapping("/{id}")
@Operation(summary = "获得水表")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('water:meter:query')")
public CommonResult<MeterRespVO> getMeter(@PathVariable("id") Long id) {
MeterDO meter = meterService.getMeter(id);
return success(BeanUtils.toBean(meter, MeterRespVO.class));
}
}
```
#### 3.2.2 抄表记录创建接口
**功能描述**:创建新的抄表记录。
**接口详情**
- **请求方式**POST
- **请求路径**`/admin-api/water/reading/create`
**请求参数**
```json
{
"meterId": 1,
"readingDate": "2024-12-19",
"readingValue": 156.32,
"readingType": "MANUAL",
"readerId": "R001",
"photoUrl": "https://example.com/photos/reading001.jpg",
"remark": "正常抄表"
}
```
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": 1
}
```
**Service层实现示例**
```java
@Service
@Validated
public class MeterReadingServiceImpl implements MeterReadingService {
@Resource
private MeterReadingMapper readingMapper;
@Resource
private MeterService meterService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createReading(MeterReadingSaveReqVO createReqVO) {
// 校验水表存在性
MeterDO meter = meterService.validateMeterExists(createReqVO.getMeterId());
// 校验读数合理性
validateReadingValue(createReqVO.getMeterId(), createReqVO.getReadingValue());
// 创建抄表记录
MeterReadingDO reading = BeanUtils.toBean(createReqVO, MeterReadingDO.class);
reading.setReadingCode(generateReadingCode());
reading.setCustomerId(meter.getCustomerId());
// 计算用水量
BigDecimal waterUsage = calculateWaterUsage(meter.getCurrentReading(),
createReqVO.getReadingValue());
reading.setWaterUsage(waterUsage);
readingMapper.insert(reading);
// 更新水表当前读数
meterService.updateCurrentReading(createReqVO.getMeterId(),
createReqVO.getReadingValue());
return reading.getId();
}
}
```
#### 3.2.3 抄表数据批量导入接口
**功能描述**批量导入抄表数据支持Excel文件上传。
**接口详情**
- **请求方式**POST
- **请求路径**`/admin-api/water/reading/import`
- **Content-Type**`multipart/form-data`
**请求参数**
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| file | MultipartFile | 是 | Excel文件 | reading_data.xlsx |
| updateSupport | Boolean | 否 | 是否更新已有数据 | false |
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"successCount": 95,
"failureCount": 5,
"failureList": [
{
"lineNumber": 3,
"meterCode": "M003",
"errorMsg": "水表不存在"
}
]
}
}
```
### 3.3 账单管理API接口
#### 3.3.1 账单查询接口
**功能描述**根据客户ID和查询条件查询账单信息。
**接口详情**
- **请求方式**GET
- **请求路径**`/admin-api/water/bill/page`
**请求参数**
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| pageNo | Integer | 否 | 页码默认1 | 1 |
| pageSize | Integer | 否 | 每页条数默认10 | 10 |
| customerId | Long | 否 | 客户ID | 1 |
| billMonth | String | 否 | 账期 | 2024-12 |
| billStatus | Integer | 否 | 账单状态 | 0 |
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"list": [
{
"id": 1,
"billCode": "B202412190001",
"billMonth": "2024-12",
"billDate": "2024-12-19",
"waterUsage": 25.50,
"waterFee": 76.50,
"sewageFee": 15.30,
"totalAmount": 91.80,
"paidAmount": 0.00,
"balanceAmount": 91.80,
"dueDate": "2025-01-19",
"billStatus": 0,
"customerName": "张三",
"meterCode": "M001"
}
],
"total": 1
}
}
```
#### 3.3.2 账单生成接口
**功能描述**:根据抄表记录生成水费账单。
**接口详情**
- **请求方式**POST
- **请求路径**`/admin-api/water/bill/generate`
**请求参数**
```json
{
"billMonth": "2024-12",
"customerIds": [1, 2, 3],
"readingIds": [1, 2, 3],
"dueDate": "2025-01-19"
}
```
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"generateCount": 3,
"successList": [
{
"customerId": 1,
"billId": 1,
"totalAmount": 91.80
}
],
"failureList": []
}
}
```
**Service层代码示例**
```java
@Service
@Validated
public class BillServiceImpl implements BillService {
@Resource
private BillMapper billMapper;
@Resource
private MeterReadingService readingService;
@Resource
private WaterPriceService priceService;
@Override
@Transactional(rollbackFor = Exception.class)
public BillGenerateRespVO generateBills(BillGenerateReqVO generateReqVO) {
BillGenerateRespVO result = new BillGenerateRespVO();
List<BillGenerateDetailVO> successList = new ArrayList<>();
List<BillGenerateDetailVO> failureList = new ArrayList<>();
for (Long readingId : generateReqVO.getReadingIds()) {
try {
// 获取抄表记录
MeterReadingDO reading = readingService.getReading(readingId);
// 计算水费
WaterFeeCalculateDTO feeResult = priceService.calculateWaterFee(
reading.getCustomerId(), reading.getWaterUsage());
// 创建账单
BillDO bill = new BillDO();
bill.setBillCode(generateBillCode());
bill.setBillMonth(generateReqVO.getBillMonth());
bill.setCustomerId(reading.getCustomerId());
bill.setMeterId(reading.getMeterId());
bill.setReadingId(readingId);
bill.setWaterUsage(reading.getWaterUsage());
bill.setWaterFee(feeResult.getWaterFee());
bill.setSewageFee(feeResult.getSewageFee());
bill.setTotalAmount(feeResult.getTotalAmount());
bill.setDueDate(generateReqVO.getDueDate());
billMapper.insert(bill);
successList.add(buildSuccessDetail(reading.getCustomerId(),
bill.getId(), feeResult.getTotalAmount()));
} catch (Exception e) {
failureList.add(buildFailureDetail(readingId, e.getMessage()));
}
}
result.setGenerateCount(successList.size());
result.setSuccessList(successList);
result.setFailureList(failureList);
return result;
}
}
```
### 3.4 缴费管理API接口
#### 3.4.1 缴费处理接口
**功能描述**:处理客户缴费操作。
**接口详情**
- **请求方式**POST
- **请求路径**`/admin-api/water/payment/create`
**请求参数**
```json
{
"customerId": 1,
"billIds": [1, 2],
"paymentType": "NORMAL",
"paymentChannel": "CASH",
"paymentAmount": 183.60,
"actualAmount": 200.00,
"operatorId": "OP001",
"outletCode": "OUT001",
"remark": "现金缴费"
}
```
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"paymentId": 1,
"paymentCode": "P202412190001",
"changeAmount": 16.40,
"invoiceNo": "INV20241219001"
}
}
```
#### 3.4.2 在线支付接口
**功能描述**:处理在线支付(微信、支付宝等)。
**接口详情**
- **请求方式**POST
- **请求路径**`/admin-api/water/payment/online-pay`
**请求参数**
```json
{
"customerId": 1,
"billIds": [1],
"paymentChannel": "WECHAT",
"paymentAmount": 91.80,
"returnUrl": "https://water.example.com/payment/callback",
"notifyUrl": "https://water.example.com/api/payment/notify"
}
```
**响应参数**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"paymentCode": "P202412190002",
"prepayId": "wx20241219001234567890",
"payUrl": "weixin://wxpay/bizpayurl?pr=abc123",
"qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}
}
```
### 3.4 工单接口
#### 3.4.1 工单创建接口
**功能描述**:创建业务工单。
**接口规范**
- 请求方式POST
- 请求路径:/api/workorders
- 请求/返回格式JSON
#### 3.4.2 工单状态更新接口
**功能描述**:更新工单处理状态。
**接口规范**
- 请求方式PUT
- 请求路径:/api/workorders/{workorderId}/status
- 请求/返回格式JSON
## 4. 接口标准
### 4.1 接口协议
系统接口主要采用以下协议:
- **RESTful API**:适用于系统内部模块间的交互以及移动应用等轻量级客户端
- **WebService**:适用于与外部系统的集成,特别是银行等传统机构
- **消息队列**:适用于异步处理的场景,如批量数据处理、通知推送等
### 4.2 数据格式
接口数据主要采用以下格式:
- **JSON**主要用于RESTful API接口结构简单清晰适合Web应用
- **XML**主要用于WebService接口兼容性好适合与传统系统对接
- **文本文件**:主要用于批量数据交换,如银行代扣文件等
### 4.3 接口安全设计
接口安全采用多层防护机制:
#### 4.3.1 认证机制
**JWT令牌认证**
```java
@RestController
public class AuthController {
@Resource
private AuthService authService;
@PostMapping("/admin-api/system/auth/login")
public CommonResult<AuthLoginRespVO> login(@Valid @RequestBody AuthLoginReqVO reqVO) {
// 验证用户名密码
AdminUserDO user = authService.authenticate(reqVO.getUsername(), reqVO.getPassword());
// 生成JWT Token
String token = authService.createToken(user.getId(), user.getTenantId());
return success(AuthLoginRespVO.builder()
.userId(user.getId())
.accessToken(token)
.refreshToken(authService.createRefreshToken(user.getId()))
.expiresTime(LocalDateTime.now().plusHours(2))
.build());
}
}
```
**API Key认证**(外部系统):
```java
@Component
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String apiKey = request.getHeader("X-API-KEY");
String timestamp = request.getHeader("X-TIMESTAMP");
String signature = request.getHeader("X-SIGNATURE");
// 验证API Key
if (!apiKeyService.validateApiKey(apiKey)) {
writeErrorResponse(response, "Invalid API Key");
return;
}
// 验证时间戳(防重放攻击)
if (!validateTimestamp(timestamp)) {
writeErrorResponse(response, "Request expired");
return;
}
// 验证签名
if (!validateSignature(request, signature)) {
writeErrorResponse(response, "Invalid signature");
return;
}
filterChain.doFilter(request, response);
}
}
```
#### 4.3.2 数据加密
**敏感数据加密**
```java
@Component
public class DataEncryptionService {
private final AESUtil aesUtil;
public String encryptPersonalInfo(String plainText) {
if (StrUtil.isBlank(plainText)) {
return plainText;
}
return aesUtil.encrypt(plainText);
}
public String decryptPersonalInfo(String cipherText) {
if (StrUtil.isBlank(cipherText)) {
return cipherText;
}
return aesUtil.decrypt(cipherText);
}
}
```
#### 4.3.3 访问控制
**IP白名单控制**
```java
@Component
public class IpWhitelistFilter extends OncePerRequestFilter {
@Value("${water.security.ip-whitelist}")
private List<String> ipWhitelist;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String clientIp = getClientIpAddress(request);
if (!isIpAllowed(clientIp)) {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("{\"code\":403,\"msg\":\"IP access denied\"}");
return;
}
filterChain.doFilter(request, response);
}
}
```
#### 4.3.4 接口限流
**基于Redis的令牌桶限流**
```java
@Component
public class RateLimitService {
@Resource
private StringRedisTemplate redisTemplate;
public boolean allowRequest(String key, int maxRequests, Duration window) {
String redisKey = "rate_limit:" + key;
String script = """
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local current = redis.call('get', key)
if current == false then
redis.call('setex', key, window, 1)
return 1
end
if tonumber(current) < limit then
return redis.call('incr', key)
else
return 0
end
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(redisKey),
String.valueOf(window.getSeconds()),
String.valueOf(maxRequests));
return result != null && result > 0;
}
}
```
### 4.4 错误处理机制
#### 4.4.1 统一异常处理
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
log.info("[serviceExceptionHandler]", ex);
return CommonResult.error(ex.getCode(), ex.getMessage());
}
@ExceptionHandler(ConstraintViolationException.class)
public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
log.info("[constraintViolationExceptionHandler]", ex);
return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确:" + ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
log.info("[methodArgumentNotValidExceptionHandler]", ex);
FieldError fieldError = ex.getBindingResult().getFieldError();
assert fieldError != null;
return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确:" + fieldError.getDefaultMessage());
}
}
```
#### 4.4.2 错误码定义
```java
public interface ErrorCodeConstants {
// ========== 通用错误码 1-000-000-000 ==========
ErrorCode SUCCESS = new ErrorCode(0, "成功");
ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
// ========== 客户管理错误码 1-001-000-000 ==========
ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_001_000_001, "客户不存在");
ErrorCode CUSTOMER_CODE_DUPLICATE = new ErrorCode(1_001_000_002, "客户编号已存在");
ErrorCode CUSTOMER_STATUS_INVALID = new ErrorCode(1_001_000_003, "客户状态不正确");
// ========== 水表管理错误码 1-002-000-000 ==========
ErrorCode METER_NOT_EXISTS = new ErrorCode(1_002_000_001, "水表不存在");
ErrorCode METER_CODE_DUPLICATE = new ErrorCode(1_002_000_002, "水表编号已存在");
ErrorCode METER_READING_INVALID = new ErrorCode(1_002_000_003, "水表读数不正确");
// ========== 账单管理错误码 1-003-000-000 ==========
ErrorCode BILL_NOT_EXISTS = new ErrorCode(1_003_000_001, "账单不存在");
ErrorCode BILL_ALREADY_PAID = new ErrorCode(1_003_000_002, "账单已缴费");
ErrorCode BILL_AMOUNT_INVALID = new ErrorCode(1_003_000_003, "账单金额不正确");
// ========== 缴费管理错误码 1-004-000-000 ==========
ErrorCode PAYMENT_FAILED = new ErrorCode(1_004_000_001, "缴费失败");
ErrorCode PAYMENT_AMOUNT_INSUFFICIENT = new ErrorCode(1_004_000_002, "缴费金额不足");
ErrorCode PAYMENT_CHANNEL_UNAVAILABLE = new ErrorCode(1_004_000_003, "缴费渠道不可用");
}
```
#### 4.4.3 接口调用示例
**成功响应示例**
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"customerName": "张三"
}
}
```
**失败响应示例**
```json
{
"code": 1001000001,
"msg": "客户不存在",
"data": null
}
```
### 4.5 前端接口调用示例
#### 4.5.1 Vue3 + TypeScript接口封装
```typescript
// api/water/customer.ts
import { request } from '@/utils/request'
export interface CustomerVO {
id: number
customerCode: string
customerName: string
customerType: string
phone: string
address: string
status: number
createTime: string
}
export interface CustomerPageReqVO extends PageParam {
customerName?: string
customerCode?: string
customerType?: string
phone?: string
}
export const CustomerApi = {
// 获取客户分页
getCustomerPage: (params: CustomerPageReqVO) => {
return request.get<PageResult<CustomerVO>>({ url: '/water/customer/page', params })
},
// 获取客户详情
getCustomer: (id: number) => {
return request.get<CustomerVO>({ url: `/water/customer/${id}` })
},
// 创建客户
createCustomer: (data: CustomerSaveReqVO) => {
return request.post<number>({ url: '/water/customer/create', data })
},
// 更新客户
updateCustomer: (data: CustomerSaveReqVO) => {
return request.put<void>({ url: '/water/customer/update', data })
},
// 删除客户
deleteCustomer: (id: number) => {
return request.delete<void>({ url: `/water/customer/delete?id=${id}` })
}
}
```
#### 4.5.2 Vue组件使用示例
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { CustomerApi, CustomerVO } from '@/api/water/customer'
import { formatDate } from '@/utils/formatTime'
const customerList = ref<CustomerVO[]>([])
const loading = ref(true)
const total = ref(0)
const queryParams = ref({
pageNo: 1,
pageSize: 10,
customerName: '',
customerCode: ''
})
const getCustomerList = async () => {
loading.value = true
try {
const data = await CustomerApi.getCustomerPage(queryParams.value)
customerList.value = data.list
total.value = data.total
} catch (error) {
console.error('获取客户列表失败:', error)
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.value.pageNo = 1
getCustomerList()
}
const handleReset = () => {
queryParams.value = {
pageNo: 1,
pageSize: 10,
customerName: '',
customerCode: ''
}
getCustomerList()
}
onMounted(() => {
getCustomerList()
})
</script>
<template>
<div class="app-container">
<!-- 查询表单 -->
<el-form :model="queryParams" ref="queryFormRef" inline>
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" />
</el-form-item>
<el-form-item label="客户编号" prop="customerCode">
<el-input v-model="queryParams.customerCode" placeholder="请输入客户编号" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="customerList">
<el-table-column label="客户编号" prop="customerCode" />
<el-table-column label="客户名称" prop="customerName" />
<el-table-column label="联系电话" prop="phone" />
<el-table-column label="创建时间" prop="createTime" :formatter="formatDate" />
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getCustomerList"
/>
</div>
</template>
```