Procházet zdrojové kódy

feat(util): 新增支付宝身份验证工具类
- 基础数据接口文档
- 添加 AliApiUtil 工具类用于身份证二要素核验
- 配置商家端 appId、私钥及证书路径
- 实现 createOrderNo 方法生成唯一订单号
- 支持 Windows 和 Linux 环境下的证书配置
- 日志记录验证过程与异常信息

wxd před 1 měsícem
rodič
revize
0e30689716

+ 484 - 0
alien-store-platform/doc/README_BASE_DATA.md

@@ -0,0 +1,484 @@
+# Web端基础数据查询接口开发文档
+
+## 一、接口概述
+
+本次开发在 `alien-store-platform` 服务中新增了基础数据查询接口,将原 `alien-store` 服务中分散在不同 Controller 的基础数据查询接口统一整合到一个 `BaseDataController` 中,方便 web 端商户平台统一管理和调用。
+
+## 二、接口列表
+
+### 2.1 根据关键词获取地址提示
+
+**接口路径:** `GET /baseData/getAddressPrompt`
+
+**接口说明:** 根据输入的地址关键词,返回高德地图的地址提示列表,支持按城市过滤
+
+**原接口路径:** `/alienStore/gaode/getInputPrompt`
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 | 示例 |
+|--------|------|------|------|------|
+| addressName | String | 是 | 地址关键词 | "中关村" |
+| city | String | 否 | 城市名称 | "北京市" |
+
+**请求示例:**
+```http
+GET /baseData/getAddressPrompt?addressName=中关村&city=北京市
+```
+
+**响应示例:**
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "操作成功",
+  "data": {
+    "status": "1",
+    "info": "OK",
+    "infocode": "10000",
+    "count": "10",
+    "tips": [
+      {
+        "id": "B000A8XZL4",
+        "name": "中关村",
+        "district": "北京市海淀区",
+        "adcode": "110108",
+        "location": "116.313676,39.982718",
+        "address": "中关村大街",
+        "typecode": "190301"
+      }
+    ]
+  }
+}
+```
+
+---
+
+### 2.2 获取行政区划数据
+
+**接口路径:** `GET /baseData/getDistrict`
+
+**接口说明:** 根据行政区划代码获取省市区数据,支持级联查询
+
+**原接口路径:** `/alienStore/gaode/getDistrict`
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 | 默认值 | 示例 |
+|--------|------|------|------|--------|------|
+| adCode | String | 否 | 行政区划代码 | 100000 | "100000" |
+
+**行政区划代码说明:**
+- `100000`:中国(返回所有省级数据)
+- `410000`:河南省(返回河南省所有市级数据)
+- `410300`:洛阳市(返回洛阳市所有区县数据)
+
+**请求示例:**
+```http
+# 查询所有省份
+GET /baseData/getDistrict?adCode=100000
+
+# 查询河南省所有城市
+GET /baseData/getDistrict?adCode=410000
+
+# 查询洛阳市所有区县
+GET /baseData/getDistrict?adCode=410300
+```
+
+**响应示例:**
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "操作成功",
+  "data": {
+    "status": "1",
+    "info": "OK",
+    "infocode": "10000",
+    "districts": [
+      {
+        "citycode": [],
+        "adcode": "100000",
+        "name": "中华人民共和国",
+        "center": "116.3683244,39.915085",
+        "level": "country",
+        "districts": [
+          {
+            "citycode": "010",
+            "adcode": "110000",
+            "name": "北京市",
+            "center": "116.405285,39.904989",
+            "level": "province"
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+---
+
+### 2.3 获取经营板块基础数据
+
+**接口路径:** `GET /baseData/getBusinessSection`
+
+**接口说明:** 获取所有经营板块数据,用于商户选择经营类型
+
+**原接口路径:** `/alienStore/store/info/getBusinessSection`
+
+**请求参数:** 无
+
+**请求示例:**
+```http
+GET /baseData/getBusinessSection
+```
+
+**响应示例:**
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "操作成功",
+  "data": [
+    {
+      "id": 1,
+      "typeName": "business_section",
+      "dictId": "1",
+      "dictDetail": "餐饮美食",
+      "parentId": 0
+    },
+    {
+      "id": 2,
+      "typeName": "business_section",
+      "dictId": "2",
+      "dictDetail": "休闲娱乐",
+      "parentId": 0
+    },
+    {
+      "id": 3,
+      "typeName": "business_section",
+      "dictId": "3",
+      "dictDetail": "生活服务",
+      "parentId": 0
+    }
+  ]
+}
+```
+
+---
+
+### 2.4 获取经营种类基础数据
+
+**接口路径:** `GET /baseData/getBusinessTypes`
+
+**接口说明:** 根据经营板块ID获取该板块下的所有经营种类
+
+**原接口路径:** `/alienStore/store/info/getBusinessSectionTypes`
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 | 示例 |
+|--------|------|------|------|------|
+| parentId | String | 是 | 经营板块ID(dictId) | "1" |
+
+**请求示例:**
+```http
+GET /baseData/getBusinessTypes?parentId=1
+```
+
+**响应示例:**
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "操作成功",
+  "data": [
+    {
+      "id": 101,
+      "typeName": "business_section",
+      "dictId": "101",
+      "dictDetail": "中餐",
+      "parentId": 1
+    },
+    {
+      "id": 102,
+      "typeName": "business_section",
+      "dictId": "102",
+      "dictDetail": "西餐",
+      "parentId": 1
+    },
+    {
+      "id": 103,
+      "typeName": "business_section",
+      "dictId": "103",
+      "dictDetail": "火锅",
+      "parentId": 1
+    },
+    {
+      "id": 104,
+      "typeName": "business_section",
+      "dictId": "104",
+      "dictDetail": "烧烤",
+      "parentId": 1
+    }
+  ]
+}
+```
+
+---
+
+## 三、业务流程说明
+
+### 3.1 地址搜索流程
+
+```
+用户输入地址关键词
+    ↓
+调用 /baseData/getAddressPrompt 接口
+    ↓
+系统查询城市编码(如果指定城市)
+    ↓
+调用高德地图输入提示接口
+    ↓
+返回地址提示列表
+```
+
+### 3.2 行政区划级联查询流程
+
+```
+1. 查询所有省份
+   GET /baseData/getDistrict?adCode=100000
+   
+2. 用户选择省份(例如:河南省,adCode=410000)
+   GET /baseData/getDistrict?adCode=410000
+   
+3. 用户选择城市(例如:洛阳市,adCode=410300)
+   GET /baseData/getDistrict?adCode=410300
+   
+4. 用户选择区县完成
+```
+
+### 3.3 经营板块和种类选择流程
+
+```
+1. 获取所有经营板块
+   GET /baseData/getBusinessSection
+   
+2. 用户选择经营板块(例如:餐饮美食,dictId=1)
+   GET /baseData/getBusinessTypes?parentId=1
+   
+3. 用户选择经营种类完成
+```
+
+---
+
+## 四、Nacos 配置要求
+
+在 Nacos 配置中心的 `alien-store-platform.yml` 配置文件中,需要添加以下高德地图相关配置:
+
+```yaml
+# 高德地图配置
+gaode:
+  # 高德地图API Key
+  key: your_gaode_api_key_here
+  
+  # 地理编码URL(地址转经纬度)
+  geoUrl: https://restapi.amap.com/v3/geocode/geo?address=%s&key=%s
+  
+  # 输入提示URL
+  geoListUrl: https://restapi.amap.com/v3/assistant/inputtips?keywords=%s&key=%s&city=%s
+  
+  # 行政区划URL
+  getDistrict: https://restapi.amap.com/v3/config/district?key=%s&keywords=%s&subdistrict=1
+  
+  # 距离计算URL
+  distanceUrl: https://restapi.amap.com/v3/direction/driving?origin=%s,%s&destination=%s,%s&key=%s
+  
+  # 直线距离URL
+  distanceTypeUrl: https://restapi.amap.com/v3/direction/driving?origin=%s,%s&destination=%s,%s&key=%s&strategy=%s
+  
+  # 周边搜索URL
+  nearUrl: https://restapi.amap.com/v3/place/around?location=%s,%s&key=%s&radius=%s&offset=%s&page=%s
+  
+  # 地铁站查询URL
+  subwayUrl: https://restapi.amap.com/v3/place/text?key=%s&location=%s,%s&keywords=%s&radius=%s&types=%s
+  
+  # 逆地理编码URL(经纬度转地址)
+  addressUrl: https://restapi.amap.com/v3/geocode/regeo?location=%s,%s&key=%s&radius=%s&extensions=%s
+```
+
+**注意事项:**
+- 需要在高德地图开放平台申请 API Key
+- 配置值可以从 `alien-store` 服务的 Nacos 配置中获取
+- 确保 API Key 有足够的调用额度
+
+---
+
+## 五、数据库依赖
+
+### 5.1 essential_city_code 表
+
+存储城市编码信息,用于地址搜索时的城市过滤。
+
+**表结构:**
+```sql
+CREATE TABLE `essential_city_code` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `area_code` varchar(20) DEFAULT NULL COMMENT '行政区划代码',
+  `area_name` varchar(100) DEFAULT NULL COMMENT '区域名称',
+  `city_code` varchar(20) DEFAULT NULL COMMENT '城市编码',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+### 5.2 store_dictionary 表
+
+存储经营板块和经营种类的字典数据。
+
+**表结构:**
+```sql
+CREATE TABLE `store_dictionary` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `type_name` varchar(50) DEFAULT NULL COMMENT '字典类型名称',
+  `dict_id` varchar(20) DEFAULT NULL COMMENT '字典ID',
+  `dict_detail` varchar(200) DEFAULT NULL COMMENT '字典详情',
+  `parent_id` int(11) DEFAULT NULL COMMENT '父节点ID',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+**数据说明:**
+- `type_name = 'business_section'` 表示经营板块/经营种类
+- `dict_id = '0'` 表示根节点(不显示)
+- `dict_id != '0' AND parent_id = 根节点ID` 表示经营板块
+- `parent_id = 经营板块ID` 表示经营种类
+
+---
+
+## 六、代码结构
+
+### 6.1 新增文件列表
+
+```
+alien-store-platform/
+├── src/main/java/shop/alien/storeplatform/
+│   ├── controller/
+│   │   └── BaseDataController.java               # 基础数据查询控制器
+│   ├── service/
+│   │   ├── BaseDataService.java                  # 基础数据服务接口
+│   │   └── impl/
+│   │       └── BaseDataServiceImpl.java          # 基础数据服务实现
+│   └── util/
+│       └── GaoDeMapApiUtil.java                  # 高德地图API工具类
+```
+
+### 6.2 核心类说明
+
+**1. BaseDataController**
+- 提供4个基础数据查询接口
+- 统一的接口路径前缀 `/baseData`
+- 完整的 Swagger API 文档
+
+**2. BaseDataService & BaseDataServiceImpl**
+- 实现基础数据查询业务逻辑
+- 调用高德地图API和数据库查询
+- 数据格式转换和封装
+
+**3. GaoDeMapApiUtil**
+- 封装高德地图API调用
+- 支持地址提示和行政区划查询
+- HTTP请求处理和JSON解析
+
+---
+
+## 七、接口对比表
+
+| 功能 | 原接口(alien-store) | 新接口(alien-store-platform) |
+|------|----------------------|-------------------------------|
+| 地址提示 | `/gaode/getInputPrompt` | `/baseData/getAddressPrompt` |
+| 行政区划 | `/gaode/getDistrict` | `/baseData/getDistrict` |
+| 经营板块 | `/store/info/getBusinessSection` | `/baseData/getBusinessSection` |
+| 经营种类 | `/store/info/getBusinessSectionTypes` | `/baseData/getBusinessTypes` |
+
+---
+
+## 八、测试用例
+
+### 8.1 地址提示测试
+
+```bash
+# 测试1:不指定城市
+curl -X GET "http://localhost:8080/baseData/getAddressPrompt?addressName=中关村"
+
+# 测试2:指定城市
+curl -X GET "http://localhost:8080/baseData/getAddressPrompt?addressName=中关村&city=北京市"
+```
+
+### 8.2 行政区划测试
+
+```bash
+# 测试1:查询所有省份
+curl -X GET "http://localhost:8080/baseData/getDistrict?adCode=100000"
+
+# 测试2:查询河南省所有城市
+curl -X GET "http://localhost:8080/baseData/getDistrict?adCode=410000"
+
+# 测试3:查询洛阳市所有区县
+curl -X GET "http://localhost:8080/baseData/getDistrict?adCode=410300"
+```
+
+### 8.3 经营数据测试
+
+```bash
+# 测试1:查询所有经营板块
+curl -X GET "http://localhost:8080/baseData/getBusinessSection"
+
+# 测试2:查询板块ID为1的所有经营种类
+curl -X GET "http://localhost:8080/baseData/getBusinessTypes?parentId=1"
+```
+
+---
+
+## 九、技术亮点
+
+1. **接口统一管理**:将分散的基础数据查询接口统一到一个 Controller 中
+2. **命名规范化**:采用更符合 web 端语义的接口路径命名
+3. **代码复用**:复用原有业务逻辑,保证数据一致性
+4. **服务解耦**:独立的工具类实现,不依赖 alien-store 服务
+5. **文档完整**:提供详细的接口文档和使用说明
+
+---
+
+## 十、部署检查清单
+
+- [ ] 确认 Nacos 配置中已添加高德地图相关配置
+- [ ] 确认高德地图 API Key 有效且有调用额度
+- [ ] 确认数据库中 `essential_city_code` 表数据完整
+- [ ] 确认数据库中 `store_dictionary` 表数据完整
+- [ ] 确认 alien-entity 模块版本一致
+- [ ] 确认接口权限配置正确
+
+---
+
+## 十一、常见问题
+
+### Q1: 高德地图API返回为空怎么办?
+A: 检查以下几点:
+1. API Key 是否有效
+2. 网络连接是否正常
+3. 调用次数是否超限
+4. 请求参数是否正确
+
+### Q2: 行政区划数据查询不准确?
+A: 确保 adCode 参数正确,可以参考高德地图的行政区划代码表
+
+### Q3: 经营种类查询为空?
+A: 检查 parentId 是否正确,确认数据库中该板块下是否有经营种类数据
+
+---
+
+**开发完成时间:** 2025-01-xx  
+**开发人员:** AI Assistant  
+**版本号:** v1.0.0
+

+ 1 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/controller/BaseDataController.java

@@ -72,3 +72,4 @@ public class BaseDataController {
     }
 }
 
+

+ 1 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/service/BaseDataService.java

@@ -47,3 +47,4 @@ public interface BaseDataService {
     R<List<StoreDictionaryVo>> getBusinessTypes(String parentId);
 }
 
+

+ 146 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/util/AliApiUtil.java

@@ -0,0 +1,146 @@
+package shop.alien.storeplatform.util;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.DatadigitalFincloudGeneralsaasTwometaCheckModel;
+import com.alipay.api.request.DatadigitalFincloudGeneralsaasTwometaCheckRequest;
+import com.alipay.api.response.DatadigitalFincloudGeneralsaasTwometaCheckResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import shop.alien.util.common.RandomCreateUtil;
+import shop.alien.util.system.OSUtil;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * 支付宝身份验证工具类
+ *
+ * @author ssk
+ * @version 1.0
+ * @since 2025-01-xx
+ */
+@Slf4j
+@Component
+public class AliApiUtil {
+
+    /**
+     * 商家端appId
+     */
+    @Value("${app.business.appId}")
+    private String businessAppId;
+
+    /**
+     * 商家端app私钥
+     */
+    @Value("${app.business.appPrivateKey}")
+    private String businessAppPrivateKey;
+
+    /**
+     * windows应用公钥证书文件路径
+     */
+    @Value("${app.business.win.appCertPath}")
+    private String businessWinAppCertPath;
+
+    /**
+     * windows支付宝公钥证书文件路径
+     */
+    @Value("${app.business.win.alipayPublicCertPath}")
+    private String businessWinAlipayPublicCertPath;
+
+    /**
+     * windows支付宝根证书文件路径
+     */
+    @Value("${app.business.win.alipayRootCertPath}")
+    private String businessWinAlipayRootCertPath;
+
+    /**
+     * linux应用公钥证书文件路径
+     */
+    @Value("${app.business.linux.appCertPath}")
+    private String businessLinuxAppCertPath;
+
+    /**
+     * linux支付宝公钥证书文件路径
+     */
+    @Value("${app.business.linux.alipayPublicCertPath}")
+    private String businessLinuxAlipayPublicCertPath;
+
+    /**
+     * linux支付宝根证书文件路径
+     */
+    @Value("${app.business.linux.alipayRootCertPath}")
+    private String businessLinuxAlipayRootCertPath;
+
+    /**
+     * 填写阿里配置
+     *
+     * @return AlipayConfig
+     */
+    private AlipayConfig setAlipayConfig() {
+        AlipayConfig alipayConfig = new AlipayConfig();
+        alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
+        alipayConfig.setAppId(businessAppId);
+        alipayConfig.setPrivateKey(businessAppPrivateKey);
+        alipayConfig.setFormat("json");
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+        if ("windows".equals(OSUtil.getOsName())) {
+            alipayConfig.setAppCertPath(businessWinAppCertPath);
+            alipayConfig.setAlipayPublicCertPath(businessWinAlipayPublicCertPath);
+            alipayConfig.setRootCertPath(businessWinAlipayRootCertPath);
+        } else {
+            alipayConfig.setAppCertPath(businessLinuxAppCertPath);
+            alipayConfig.setAlipayPublicCertPath(businessLinuxAlipayPublicCertPath);
+            alipayConfig.setRootCertPath(businessLinuxAlipayRootCertPath);
+        }
+        return alipayConfig;
+    }
+
+    /**
+     * 身份证二要素核验
+     *
+     * @param name   姓名
+     * @param idCard 身份证
+     * @return true:验证通过,false:验证失败
+     */
+    public boolean verifyIdCard(String name, String idCard) {
+        try {
+            log.info("AliApiUtil.verifyIdCard?name={}&idCard={}", name, idCard);
+            AlipayClient alipayClient = new DefaultAlipayClient(setAlipayConfig());
+            // 构造请求参数以调用接口
+            DatadigitalFincloudGeneralsaasTwometaCheckRequest request = new DatadigitalFincloudGeneralsaasTwometaCheckRequest();
+            DatadigitalFincloudGeneralsaasTwometaCheckModel model = new DatadigitalFincloudGeneralsaasTwometaCheckModel();
+            // 设置外部订单号
+            model.setOuterBizNo("id_" + createOrderNo());
+            // 设置姓名
+            model.setCertName(name);
+            // 设置证件号码
+            model.setCertNo(idCard);
+            // 设置证件类型
+            model.setCertType("IDENTITY_CARD");
+            request.setBizModel(model);
+            DatadigitalFincloudGeneralsaasTwometaCheckResponse response = alipayClient.certificateExecute(request);
+            JSONObject jsonObject = JSONObject.parseObject(response.getBody()).getJSONObject("datadigital_fincloud_generalsaas_twometa_check_response");
+            return response.isSuccess() && "Success".equals(jsonObject.getString("msg")) && "T".equals(jsonObject.getString("match"));
+        } catch (AlipayApiException e) {
+            log.error("AliApiUtil.verifyIdCard ERROR Msg={}", e.getErrMsg());
+            return false;
+        }
+    }
+
+    /**
+     * 生成订单号
+     *
+     * @return 订单号
+     */
+    private String createOrderNo() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
+        return sdf.format(new Date()) + RandomCreateUtil.getRandomNum(4);
+    }
+}
+