福建水务营收系统-接口设计

系统设计团队

2024年12月19日

1 福建水务营收系统接口设计文档

1.1 文档信息

项目信息 详情
项目名称 福建水务营收系统
文档类型 概要设计文档
技术框架 RuoYi-Vue-Pro + yudao-ui-admin-vue3
文档版本 v1.0
编写日期 2024-12-19
文档状态 🟡 进行中

1.2 目录

1.3 1. 接口概述

福建水务业务系统提供丰富的接口,用于与外部系统集成以及系统内部各模块间的数据交换。接口设计遵循标准化、安全性、可扩展性的原则,基于RuoYi-Vue-Pro框架采用RESTful风格设计,支持JSON数据格式。

1.3.1 1.1 接口设计原则

1.3.2 1.2 RESTful API规范

系统API接口采用RESTful风格设计,主要规范如下:

1.3.2.1 1.2.1 资源命名

1.3.2.2 1.2.2 HTTP方法

1.3.2.3 1.2.3 状态码

1.3.2.4 1.2.4 响应格式

系统统一采用以下JSON格式响应:

{
  "code": 0,           // 业务状态码,0表示成功,非0表示失败
  "data": {},          // 响应数据
  "msg": "success"     // 响应消息
}

分页查询响应格式:

{
  "code": 0,
  "data": {
    "list": [],        // 数据列表
    "total": 100,      // 总记录数
    "pageNum": 1,      // 当前页码
    "pageSize": 10     // 每页记录数
  },
  "msg": "success"
}

1.3.3 1.3 接口文档

系统使用Knife4j(基于Swagger)自动生成API文档,文档地址为:http://{系统地址}/doc.html

主要特点: - 在线接口文档:支持在线查看接口定义 - 接口调试:支持在线调试接口 - 文档导出:支持导出OpenAPI规范文档 - 权限控制:支持对接口文档的访问控制

1.4 2. 外部接口

1.4.1 2.1 银行接口对接

1.4.1.1 2.1.1 银行代扣接口

功能描述:通过银行系统自动从用户账户中扣除水费。

接口详情: - 接口方式:文件交换(FTP/SFTP) - 数据格式:定长文本文件 - 交换频率:每日凌晨2:00 - 文件编码:GBK

代扣文件格式

记录类型(1位) + 客户号(12位) + 户名(30位) + 银行账号(20位) + 扣款金额(12位,含2位小数) + 账期(6位) + 保留字段(19位)

代扣文件示例

1C00000000001张三                          62172511001234567890000009180202412          
1C00000000002李四                          62172511001234567891000015460202412          

回盘文件格式

记录类型(1位) + 客户号(12位) + 银行账号(20位) + 扣款金额(12位) + 处理状态(1位) + 银行流水号(20位) + 处理时间(14位) + 失败原因(20位)

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("
");
        }
        return content.toString();
    }
}

1.4.1.2 2.1.2 银行实时缴费接口

功能描述:用户在银行柜台、网上银行或手机银行实时缴纳水费。

接口详情: - 接口方式:HTTP POST - 请求URLhttps://bank.api.com/payment/water-fee - 数据格式:JSON - 认证方式:API Key + 签名

请求参数

{
  "merchantId": "WATER001",
  "customerCode": "C001",
  "billCodes": ["B202412190001"],
  "totalAmount": 91.80,
  "bankAccount": "6217251100123456789",
  "customerName": "张三",
  "timestamp": "20241219103000",
  "signature": "ABC123DEF456..."
}

响应参数

{
  "resultCode": "0000",
  "resultMsg": "交易成功",
  "data": {
    "transactionId": "TXN20241219001",
    "paymentTime": "20241219103001",
    "bankSerial": "BNK20241219001234"
  }
}

1.4.2 2.2 支付宝接口对接

功能描述:用户通过支付宝缴纳水费,支持扫码支付和H5支付。

接口详情: - 接口方式:HTTP POST - 支付方式:统一收单交易预创建(alipay.trade.precreate) - 数据格式:JSON - 认证方式:RSA2签名

预创建支付请求参数

{
  "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"
  }
}

支付宝响应参数

{
  "alipay_trade_precreate_response": {
    "code": "10000",
    "msg": "Success",
    "out_trade_no": "P202412190002",
    "qr_code": "https://qr.alipay.com/bax08945xtdnfwgqmwi200b4"
  },
  "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
}

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());
        }
    }
}

1.4.3 2.3 微信支付接口对接

功能描述:用户通过微信支付缴纳水费,支持扫码支付和小程序支付。

接口详情: - 接口方式:HTTP POST - 支付方式:Native支付(扫码)/ JSAPI支付(小程序) - 请求URLhttps://api.mch.weixin.qq.com/v3/pay/transactions/native - 数据格式:JSON - 认证方式:微信支付V3签名

统一下单请求参数

{
  "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"
}

微信支付响应参数

{
  "code_url": "weixin://wxpay/bizpayurl?pr=HuaLcAKwa"
}

支付结果通知参数

{
  "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": "..."
  }
}

1.4.4 2.4 短信接口

功能描述:向用户发送各类业务通知短信。

接口规范: - 接口方式:HTTP接口 - 数据格式:JSON - 交换频率:实时

1.4.5 2.5 物联网集抄平台接口

功能描述:与物联网集抄平台交互,获取智能水表数据。

接口规范: - 接口方式:HTTP接口或WebService - 数据格式:JSON或XML - 交换频率:定时或实时

1.5 3. 内部接口

1.5.1 3.1 客户管理API接口

1.5.1.1 3.1.1 客户信息查询接口

功能描述:根据客户ID查询客户详细信息。

接口详情: - 请求方式:GET - 请求路径/admin-api/water/customer/{id} - 请求头Authorization: Bearer {token}

请求参数: | 参数名 | 类型 | 必填 | 说明 | 示例 | |——-|——|——|——|——| | id | Long | 是 | 客户ID | 1 |

响应参数

{
  "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代码示例

@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));
    }
}

1.5.1.2 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 |

响应参数

{
  "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
  }
}

1.5.1.3 3.1.3 客户创建接口

功能描述:创建新客户记录。

接口详情: - 请求方式:POST - 请求路径/admin-api/water/customer/create

请求参数

{
  "customerCode": "C002",
  "customerName": "李四",
  "customerType": "RESIDENT",
  "idType": "ID_CARD",
  "idNumber": "350103199001011234",
  "phone": "13900139000",
  "address": "福建省福州市鼓楼区XX街道XX号"
}

响应参数

{
  "code": 0,
  "msg": "操作成功",
  "data": 2
}

Service层代码示例

@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);
        }
    }
}

1.5.2 3.2 水表管理API接口

1.5.2.1 3.2.1 水表信息查询接口

功能描述:根据水表ID查询水表详细信息。

接口详情: - 请求方式:GET - 请求路径/admin-api/water/meter/{id} - 请求头Authorization: Bearer {token}

请求参数: | 参数名 | 类型 | 必填 | 说明 | 示例 | |——-|——|——|——|——| | id | Long | 是 | 水表ID | 1 |

响应参数

{
  "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代码示例

@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));
    }
}

1.5.2.2 3.2.2 抄表记录创建接口

功能描述:创建新的抄表记录。

接口详情: - 请求方式:POST - 请求路径/admin-api/water/reading/create

请求参数

{
  "meterId": 1,
  "readingDate": "2024-12-19",
  "readingValue": 156.32,
  "readingType": "MANUAL",
  "readerId": "R001",
  "photoUrl": "https://example.com/photos/reading001.jpg",
  "remark": "正常抄表"
}

响应参数

{
  "code": 0,
  "msg": "操作成功", 
  "data": 1
}

Service层实现示例

@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();
    }
}

1.5.2.3 3.2.3 抄表数据批量导入接口

功能描述:批量导入抄表数据,支持Excel文件上传。

接口详情: - 请求方式:POST - 请求路径/admin-api/water/reading/import - Content-Typemultipart/form-data

请求参数: | 参数名 | 类型 | 必填 | 说明 | 示例 | |——-|——|——|——|——| | file | MultipartFile | 是 | Excel文件 | reading_data.xlsx | | updateSupport | Boolean | 否 | 是否更新已有数据 | false |

响应参数

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "successCount": 95,
    "failureCount": 5,
    "failureList": [
      {
        "lineNumber": 3,
        "meterCode": "M003", 
        "errorMsg": "水表不存在"
      }
    ]
  }
}

1.5.3 3.3 账单管理API接口

1.5.3.1 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 |

响应参数

{
  "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
  }
}

1.5.3.2 3.3.2 账单生成接口

功能描述:根据抄表记录生成水费账单。

接口详情: - 请求方式:POST - 请求路径/admin-api/water/bill/generate

请求参数

{
  "billMonth": "2024-12",
  "customerIds": [1, 2, 3],
  "readingIds": [1, 2, 3],
  "dueDate": "2025-01-19"
}

响应参数

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "generateCount": 3,
    "successList": [
      {
        "customerId": 1,
        "billId": 1,
        "totalAmount": 91.80
      }
    ],
    "failureList": []
  }
}

Service层代码示例

@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;
    }
}

1.5.4 3.4 缴费管理API接口

1.5.4.1 3.4.1 缴费处理接口

功能描述:处理客户缴费操作。

接口详情: - 请求方式:POST - 请求路径/admin-api/water/payment/create

请求参数

{
  "customerId": 1,
  "billIds": [1, 2],
  "paymentType": "NORMAL",
  "paymentChannel": "CASH",
  "paymentAmount": 183.60,
  "actualAmount": 200.00,
  "operatorId": "OP001",
  "outletCode": "OUT001",
  "remark": "现金缴费"
}

响应参数

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "paymentId": 1,
    "paymentCode": "P202412190001",
    "changeAmount": 16.40,
    "invoiceNo": "INV20241219001"
  }
}

1.5.4.2 3.4.2 在线支付接口

功能描述:处理在线支付(微信、支付宝等)。

接口详情: - 请求方式:POST - 请求路径/admin-api/water/payment/online-pay

请求参数

{
  "customerId": 1,
  "billIds": [1],
  "paymentChannel": "WECHAT",
  "paymentAmount": 91.80,
  "returnUrl": "https://water.example.com/payment/callback",
  "notifyUrl": "https://water.example.com/api/payment/notify"
}

响应参数

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "paymentCode": "P202412190002",
    "prepayId": "wx20241219001234567890",
    "payUrl": "weixin://wxpay/bizpayurl?pr=abc123",
    "qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
  }
}

1.5.5 3.4 工单接口

1.5.5.1 3.4.1 工单创建接口

功能描述:创建业务工单。

接口规范: - 请求方式:POST - 请求路径:/api/workorders - 请求/返回格式:JSON

1.5.5.2 3.4.2 工单状态更新接口

功能描述:更新工单处理状态。

接口规范: - 请求方式:PUT - 请求路径:/api/workorders/{workorderId}/status - 请求/返回格式:JSON

1.6 4. 接口标准

1.6.1 4.1 接口协议

系统接口主要采用以下协议:

1.6.2 4.2 数据格式

接口数据主要采用以下格式:

1.6.3 4.3 接口安全设计

接口安全采用多层防护机制:

1.6.3.1 4.3.1 认证机制

JWT令牌认证

@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认证(外部系统):

@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);
    }
}

1.6.3.2 4.3.2 数据加密

敏感数据加密

@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);
    }
}

1.6.3.3 4.3.3 访问控制

IP白名单控制

@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);
    }
}

1.6.3.4 4.3.4 接口限流

基于Redis的令牌桶限流

@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;
    }
}

1.6.4 4.4 错误处理机制

1.6.4.1 4.4.1 统一异常处理

@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());
    }
}

1.6.4.2 4.4.2 错误码定义

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, "缴费渠道不可用");
}

1.6.4.3 4.4.3 接口调用示例

成功响应示例

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "id": 1,
    "customerName": "张三"
  }
}

失败响应示例

{
  "code": 1001000001,
  "msg": "客户不存在",
  "data": null
}

1.6.5 4.5 前端接口调用示例

1.6.5.1 4.5.1 Vue3 + 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}` })
  }
}

1.6.5.2 4.5.2 Vue组件使用示例

```vue