zbb преди 1 месец
ревизия
4fa68d0bb1

+ 99 - 0
QUICK_START.md

@@ -0,0 +1,99 @@
+# 快速开始指南 - VnPay Demo
+
+## 🚀 5分钟快速启动
+
+### Windows用户
+
+1. **双击运行 `start.bat`**
+   - 自动检查环境
+   - 自动编译项目
+   - 自动启动服务
+
+### Mac/Linux用户
+
+```bash
+chmod +x start.sh
+./start.sh
+```
+
+## 📝 快速测试
+
+### 1. 验证服务状态
+
+打开浏览器访问:http://localhost:8080
+
+你应该能看到项目的API文档页面。
+
+### 2. 使用Postman测试
+
+#### 方法一:导入Postman集合
+
+1. 打开Postman
+2. 点击 Import
+3. 选择文件 `VnPay-Demo.postman_collection.json`
+4. 运行测试
+
+#### 方法二:手动测试
+
+**测试服务状态:**
+```
+GET http://localhost:8080/demo/test
+```
+
+**测试支付接口:**
+```
+POST http://localhost:8080/demo/apiPay
+Content-Type: application/x-www-form-urlencoded
+
+tradeType=SCAN_PAY&version=1.0&channel=ALI_PAY&mchNo=M1234567890&mchOrderNo=ORDER20240130001&body=测试商品&amount=10000&currency=VND&timePaid=20240130120000&notifyUrl=http://localhost:8080/demo/notify/getPayInfo&paySecret=bf183091a6654c339b7b452a996c4ce5
+```
+
+### 3. 使用cURL测试
+
+```bash
+# 测试服务状态
+curl http://localhost:8080/demo/test
+
+# 测试支付接口
+curl -X POST http://localhost:8080/demo/apiPay \
+  -H "Content-Type: application/x-www-form-urlencoded" \
+  -d "tradeType=SCAN_PAY&version=1.0&channel=ALI_PAY&mchNo=M1234567890&mchOrderNo=ORDER$(date +%s)&body=测试商品&amount=10000&currency=VND&timePaid=$(date +%Y%m%d%H%M%S)&notifyUrl=http://localhost:8080/demo/notify/getPayInfo&paySecret=bf183091a6654c339b7b452a996c4ce5"
+```
+
+## 🎯 关键测试点
+
+1. **签名验证** - 确保请求包含正确的签名
+2. **订单号唯一性** - 每次测试使用不同的订单号
+3. **异步通知** - 确保notifyUrl可访问
+4. **金额单位** - 注意金额单位是分,不是元
+
+## 🔧 常见问题
+
+### 端口被占用?
+
+修改 `application.properties` 中的 `server.port=8080` 为其他端口。
+
+### 编译失败?
+
+检查:
+- JDK版本是否为1.8+
+- Maven是否正确安装
+- 网络是否能访问Maven仓库
+
+### 签名错误?
+
+1. 查看控制台日志中的"签名前字符串"
+2. 确认参数排序正确
+3. 确认paySecret密钥正确
+
+## 📞 需要帮助?
+
+1. 查看控制台日志
+2. 访问 http://localhost:8080 查看API文档
+3. 查看项目中的API文档:
+   - `Api-V1.8.13 (19) (2).docx`
+   - `Api-V1.8.13EN.docx`
+
+---
+
+祝测试顺利!🎉

+ 187 - 0
README.md

@@ -0,0 +1,187 @@
+# 越南支付对接Demo - Spring Boot版本
+
+这是一个基于Spring Boot的越南支付对接Demo项目,提供了完整的支付接口示例和测试方法。
+
+## 项目特点
+
+- ✅ **一键启动** - 基于Spring Boot,内嵌Tomcat,无需额外配置
+- ✅ **RESTful API** - 提供标准的REST接口,方便测试
+- ✅ **完整示例** - 包含支付请求、异步通知等完整流程
+- ✅ **详细文档** - 内置API文档页面,方便查看和测试
+
+## 快速开始
+
+### 1. 环境要求
+
+- JDK 1.8+
+- Maven 3.x+
+
+### 2. 启动项目
+
+```bash
+# 进入项目目录
+cd springboot-vnpay-demo
+
+# 编译项目
+mvn clean package
+
+# 运行项目
+mvn spring-boot:run
+
+# 或者直接运行JAR包
+java -jar target/springboot-vnpay-demo-1.0.0.jar
+```
+
+### 3. 访问项目
+
+启动成功后,访问以下地址:
+
+- **项目首页**: http://localhost:8080
+- **测试接口**: http://localhost:8080/demo/test
+
+## API接口说明
+
+### 1. 测试接口
+
+```
+GET /demo/test
+```
+
+用于测试服务是否正常运行。
+
+### 2. 支付接口
+
+```
+POST /demo/apiPay
+Content-Type: application/x-www-form-urlencoded
+```
+
+**请求参数:**
+
+| 参数名 | 必填 | 说明 | 示例值 |
+|--------|------|------|--------|
+| tradeType | 是 | 支付类型 | SCAN_PAY |
+| version | 是 | 版本号 | 1.0 |
+| channel | 是 | 支付渠道 | ALI_PAY |
+| mchNo | 是 | 商户号 | M1234567890 |
+| mchOrderNo | 是 | 商户订单号 | ORDER20240130001 |
+| body | 是 | 商品描述 | 测试商品 |
+| amount | 是 | 金额(分) | 10000 |
+| currency | 是 | 货币类型 | VND |
+| timePaid | 是 | 支付时间 | 20240130120000 |
+| notifyUrl | 是 | 异步通知地址 | http://your-server.com/notify |
+| paySecret | 是 | 支付密钥 | bf183091a6654c339b7b452a996c4ce5 |
+
+### 3. 异步通知接口
+
+```
+POST /demo/notify/getPayInfo
+Content-Type: application/json
+```
+
+**请求体示例:**
+
+```json
+{
+    "mchNo": "M1234567890",
+    "mchOrderNo": "ORDER20240130001",
+    "amount": "10000",
+    "status": "1",
+    "sign": "XXXXXXXXXXXXXXXXXXXXX"
+}
+```
+
+## Postman测试
+
+### 导入Postman
+
+1. 打开Postman
+2. 创建新的请求
+3. 设置请求方法为POST
+4. URL: `http://localhost:8080/demo/apiPay`
+5. Body选择`x-www-form-urlencoded`
+6. 添加上述参数
+
+### 测试示例
+
+```bash
+# 使用curl测试
+curl -X POST http://localhost:8080/demo/apiPay \
+  -H "Content-Type: application/x-www-form-urlencoded" \
+  -d "tradeType=SCAN_PAY&version=1.0&channel=ALI_PAY&mchNo=M1234567890&mchOrderNo=ORDER$(date +%Y%m%d%H%M%S)&body=测试商品&amount=10000&currency=VND&timePaid=$(date +%Y%m%d%H%M%S)&notifyUrl=http://localhost:8080/demo/notify/getPayInfo&paySecret=bf183091a6654c339b7b452a996c4ce5"
+```
+
+## 配置说明
+
+配置文件位置:`src/main/resources/application.properties`
+
+主要配置项:
+
+```properties
+# 服务端口
+server.port=8080
+
+# 商户支付KEY
+payKey=64efebf7eb5b439d8fa213de9028392e
+
+# 商户密钥
+paySecret=bf183091a6654c339b7b452a996c4ce5
+
+# API支付请求地址
+apiPayUrl=http://localhost:8080/gateway/api/trade
+```
+
+## 项目结构
+
+```
+springboot-vnpay-demo/
+├── src/
+│   ├── main/
+│   │   ├── java/
+│   │   │   └── com/vnpay/demo/
+│   │   │       ├── VnPayDemoApplication.java    # 启动类
+│   │   │       ├── controller/                  # 控制器
+│   │   │       │   ├── BaseController.java
+│   │   │       │   └── DemoPayController.java
+│   │   │       └── utils/                       # 工具类
+│   │   │           ├── HttpUtil.java
+│   │   │           ├── MD5Util.java
+│   │   │           ├── MerchantApiUtil.java
+│   │   │           ├── PayConfigUtil.java
+│   │   │           └── StringUtil.java
+│   │   └── resources/
+│   │       ├── application.properties           # 配置文件
+│   │       └── static/
+│   │           └── index.html                   # API文档页面
+├── pom.xml                                      # Maven配置
+└── README.md                                    # 本文件
+```
+
+## 注意事项
+
+1. **修改配置** - 请根据实际情况修改`application.properties`中的配置
+2. **签名验证** - 所有请求都需要正确的签名,签名规则见API文档
+3. **异步通知** - 确保`notifyUrl`可以被支付平台访问到
+4. **安全提示** - 生产环境请妥善保管`paySecret`密钥
+
+## 常见问题
+
+### Q: 如何修改服务端口?
+A: 修改`application.properties`中的`server.port`配置
+
+### Q: 签名验证失败怎么办?
+A: 检查参数排序和密钥是否正确,可以查看控制台日志中的签名前字符串
+
+### Q: 如何查看详细日志?
+A: 日志文件位于`logs/vnpay-demo.log`,或查看控制台输出
+
+## 技术支持
+
+如有问题,请参考项目中的API文档文件:
+- `Api-V1.8.13 (19) (2).docx`
+- `Api-V1.8.13EN.docx`
+
+---
+
+**版本**: 1.0.0  
+**更新时间**: 2024-01-30

+ 184 - 0
VnPay-Demo.postman_collection.json

@@ -0,0 +1,184 @@
+{
+    "info": {
+        "name": "VnPay Demo API",
+        "description": "越南支付对接Demo接口测试集合",
+        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+    },
+    "item": [
+        {
+            "name": "测试服务状态",
+            "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                    "raw": "http://localhost:8080/demo/test",
+                    "protocol": "http",
+                    "host": ["localhost"],
+                    "port": "8080",
+                    "path": ["demo", "test"]
+                },
+                "description": "检查服务是否正常运行"
+            }
+        },
+        {
+            "name": "API支付请求",
+            "request": {
+                "method": "POST",
+                "header": [
+                    {
+                        "key": "Content-Type",
+                        "value": "application/x-www-form-urlencoded"
+                    }
+                ],
+                "body": {
+                    "mode": "urlencoded",
+                    "urlencoded": [
+                        {
+                            "key": "tradeType",
+                            "value": "SCAN_PAY",
+                            "description": "支付类型"
+                        },
+                        {
+                            "key": "version",
+                            "value": "1.0",
+                            "description": "版本号"
+                        },
+                        {
+                            "key": "channel",
+                            "value": "ALI_PAY",
+                            "description": "支付渠道"
+                        },
+                        {
+                            "key": "mchNo",
+                            "value": "M1234567890",
+                            "description": "商户号"
+                        },
+                        {
+                            "key": "mchOrderNo",
+                            "value": "ORDER{{$timestamp}}",
+                            "description": "商户订单号(自动生成时间戳)"
+                        },
+                        {
+                            "key": "body",
+                            "value": "测试商品",
+                            "description": "商品描述"
+                        },
+                        {
+                            "key": "amount",
+                            "value": "10000",
+                            "description": "金额(单位:分)"
+                        },
+                        {
+                            "key": "currency",
+                            "value": "VND",
+                            "description": "货币类型"
+                        },
+                        {
+                            "key": "timePaid",
+                            "value": "{{$timestamp}}",
+                            "description": "支付时间"
+                        },
+                        {
+                            "key": "remark",
+                            "value": "测试备注",
+                            "description": "备注信息"
+                        },
+                        {
+                            "key": "notifyUrl",
+                            "value": "http://localhost:8080/demo/notify/getPayInfo",
+                            "description": "异步通知地址"
+                        },
+                        {
+                            "key": "callbackUrl",
+                            "value": "http://localhost:8080/callback",
+                            "description": "同步回调地址"
+                        },
+                        {
+                            "key": "merchantName",
+                            "value": "测试商户",
+                            "description": "商户名称"
+                        },
+                        {
+                            "key": "goodsId",
+                            "value": "GOODS001",
+                            "description": "商品ID"
+                        },
+                        {
+                            "key": "goodsDesc",
+                            "value": "这是一个测试商品",
+                            "description": "商品描述"
+                        },
+                        {
+                            "key": "showUrl",
+                            "value": "http://localhost:8080/goods/001",
+                            "description": "商品展示URL"
+                        },
+                        {
+                            "key": "memberId",
+                            "value": "USER123456",
+                            "description": "会员ID"
+                        },
+                        {
+                            "key": "bankType",
+                            "value": "BANK_CARD",
+                            "description": "银行类型"
+                        },
+                        {
+                            "key": "cardType",
+                            "value": "DEBIT",
+                            "description": "卡类型"
+                        },
+                        {
+                            "key": "orderPeriod",
+                            "value": "30",
+                            "description": "订单过期时间(分钟)"
+                        },
+                        {
+                            "key": "paySecret",
+                            "value": "bf183091a6654c339b7b452a996c4ce5",
+                            "description": "支付密钥"
+                        }
+                    ]
+                },
+                "url": {
+                    "raw": "http://localhost:8080/demo/apiPay",
+                    "protocol": "http",
+                    "host": ["localhost"],
+                    "port": "8080",
+                    "path": ["demo", "apiPay"]
+                },
+                "description": "发起支付请求"
+            }
+        },
+        {
+            "name": "异步通知测试",
+            "request": {
+                "method": "POST",
+                "header": [
+                    {
+                        "key": "Content-Type",
+                        "value": "application/json"
+                    }
+                ],
+                "body": {
+                    "mode": "raw",
+                    "raw": "{\n    \"mchNo\": \"M1234567890\",\n    \"mchOrderNo\": \"ORDER20240130001\",\n    \"amount\": \"10000\",\n    \"status\": \"1\",\n    \"payTime\": \"20240130120500\",\n    \"transactionId\": \"PAY20240130120500001\",\n    \"sign\": \"需要根据实际参数计算\"\n}"
+                },
+                "url": {
+                    "raw": "http://localhost:8080/demo/notify/getPayInfo",
+                    "protocol": "http",
+                    "host": ["localhost"],
+                    "port": "8080",
+                    "path": ["demo", "notify", "getPayInfo"]
+                },
+                "description": "模拟支付平台发送的异步通知"
+            }
+        }
+    ],
+    "variable": [
+        {
+            "key": "baseUrl",
+            "value": "http://localhost:8080"
+        }
+    ]
+}

+ 104 - 0
pom.xml

@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" 
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
+         https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.15</version>
+        <relativePath/>
+    </parent>
+    
+    <groupId>com.vnpay</groupId>
+    <artifactId>springboot-vnpay-demo</artifactId>
+    <version>1.0.0</version>
+    <name>springboot-vnpay-demo</name>
+    <description>Vietnam Payment Gateway Demo with Spring Boot</description>
+    
+    <properties>
+        <java.version>1.8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    
+    <dependencies>
+        <!-- Spring Boot Web Starter -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        
+        <!-- JSP 支持 -->
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-jasper</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>jstl</artifactId>
+        </dependency>
+        
+        <!-- Apache HttpClient -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.13</version>
+        </dependency>
+        
+        <!-- Alibaba Fastjson -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.83</version>
+        </dependency>
+        
+        <!-- Alibaba Druid -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid</artifactId>
+            <version>1.2.16</version>
+        </dependency>
+        
+        <!-- Commons Lang -->
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+        </dependency>
+        
+        <!-- Commons IO -->
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.11.0</version>
+        </dependency>
+        
+        <!-- Spring Boot DevTools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </dependency>
+        
+        <!-- Spring Boot Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 22 - 0
src/main/java/com/vnpay/demo/VnPayDemoApplication.java

@@ -0,0 +1,22 @@
+package com.vnpay.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Vietnam Payment Demo Application
+ * 越南支付对接Demo Spring Boot启动类
+ * 
+ * @author VnPay Demo
+ */
+@SpringBootApplication
+public class VnPayDemoApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(VnPayDemoApplication.class, args);
+        System.out.println("====================================");
+        System.out.println("VnPay Demo 启动成功!");
+        System.out.println("访问地址: http://localhost:8080");
+        System.out.println("====================================");
+    }
+}

+ 247 - 0
src/main/java/com/vnpay/demo/controller/BaseController.java

@@ -0,0 +1,247 @@
+package com.vnpay.demo.controller;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.web.context.ContextLoader;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import com.vnpay.demo.utils.StringUtil;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.math.BigDecimal;
+import java.net.InetAddress;
+import java.net.URLDecoder;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 基础控制器类
+ */
+public abstract class BaseController {
+
+    private static final Log log = LogFactory.getLog(BaseController.class);
+    
+    private static final String UTF_8 = "utf-8";
+    private static final String GBK = "GBK";
+
+    /**
+     * 获取request
+     */
+    protected HttpServletRequest getRequest() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+    }
+
+    /**
+     * 获取session
+     */
+    protected HttpSession getSession() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();
+    }
+    
+    /**
+     * 获取application
+     */
+    protected ServletContext getApplication() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession().getServletContext();
+    }
+
+    protected ServletContext getServletContext() {
+        return ContextLoader.getCurrentWebApplicationContext().getServletContext();
+    }
+
+    public String getString(String name) {
+        return getString(name, null);
+    }
+
+    public String getString(String name, String defaultValue) {
+        String resultStr = getRequest().getParameter(name);
+        if (resultStr == null || "".equals(resultStr) || "null".equals(resultStr) || "undefined".equals(resultStr)) {
+            return defaultValue;
+        } else {
+            return resultStr;
+        }
+    }
+    
+    /**
+     * 获取请求中的参数值,如果参数值为null刚转为空字符串""
+     */
+    public Map<String, Object> getParamMap_NullStr(Map map) {
+        Map<String, Object> parameters = new HashMap<String, Object>();
+        Set keys = map.keySet();
+        for (Object key : keys) {
+            String value = this.getString(key.toString());
+            if (value == null){
+                value = "";
+            }
+            parameters.put(key.toString(), value);
+        }
+        return parameters;
+    }
+
+    public int getInt(String name) {
+        return getInt(name, 0);
+    }
+
+    public int getInt(String name, int defaultValue) {
+        String resultStr = getRequest().getParameter(name);
+        if (resultStr != null) {
+            try {
+                return Integer.parseInt(resultStr);
+            } catch (Exception e) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    public BigDecimal getBigDecimal(String name) {
+        return getBigDecimal(name, null);
+    }
+
+    public BigDecimal getBigDecimal(String name, BigDecimal defaultValue) {
+        String resultStr = getRequest().getParameter(name);
+        if (resultStr != null) {
+            try {
+                return BigDecimal.valueOf(Double.parseDouble(resultStr));
+            } catch (Exception e) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+    
+    /**
+     * 根据参数名从HttpRequest中获取String类型的参数值,无值则返回""
+     */
+    public String getString_UrlDecode_UTF8(String key) {
+        try {
+            return URLDecoder.decode(this.getString(key), UTF_8).trim();
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    public String getString_UrlDecode_GBK(String key) {
+        try {
+            return new String(getString(key.toString()).getBytes("GBK"), "UTF-8");
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    /**
+     * 获取客户端的IP地址
+     */
+    public String getIpAddr(HttpServletRequest request) {
+        String ipAddress = null;
+        ipAddress = request.getHeader("x-forwarded-for");
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
+                // 根据网卡取本机配置的IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (UnknownHostException e) {
+                    e.printStackTrace();
+                }
+                ipAddress = inet.getHostAddress();
+            }
+        }
+
+        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ipAddress != null && ipAddress.length() > 15) {
+            if (ipAddress.indexOf(",") > 0) {
+                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+            }
+        }
+        return ipAddress;
+    }
+    
+    /**
+     * 获取refererUrl
+     */
+    public String getRefererUrl(HttpServletRequest request) {
+        return request.getHeader("referer");
+    }
+    
+    public String readRequest(HttpServletRequest request) throws IOException {
+        StringBuilder sb = new StringBuilder();
+        try {
+            String line;
+            while ((line = request.getReader().readLine()) != null) {
+                sb.append(line);
+            }
+        } finally {
+            request.getReader().close();
+        }
+        return sb.toString();
+    }
+    
+    public void writeWithContentType(HttpServletResponse response, String s) {
+        response.setContentType("text/text;charset=UTF-8");
+        write(response, s);
+    }
+    
+    public void writeWithHTMl(HttpServletResponse response, String s) {
+        response.setContentType("text/Html;charset=UTF-8");
+        write(response, s);
+    }
+    
+    public void write(HttpServletResponse response, String s) {
+        PrintWriter out = null;
+        try {
+            out = response.getWriter();
+            out.print(s);
+        } catch (IOException e) {
+            log.error("返回支付结果接收状态到微信支付错误", e);
+        } finally {
+            out.close();
+        }
+    }
+
+    /**
+     * post重定向
+     */
+    public static void RedirectByPost(HttpServletResponse response, String postUrl, Map<String, ?> paramMap){
+        response.setContentType("text/html;charset=utf-8");  
+        PrintWriter out = null;
+        if(paramMap == null){
+            paramMap = new HashMap<String, String>();
+        }
+        try{
+            out = response.getWriter();
+            if(StringUtil.isEmpty(postUrl)){
+                out.println("<form method='post' >");  
+                out.println("<h1 style=' text-align:center;padding-top:50px;color:red'>响应异常:"+paramMap.toString()+"</h1>");
+            }else{
+                out.println("<form name='postSubmit' method='post' action='"+postUrl+"' >");  
+                for (String key : paramMap.keySet()) {
+                    out.println("<input type='hidden' name='"+key+"' value='" + paramMap.get(key)+ "' />");
+                }
+            }
+            out.println("</form>");   
+            out.println("<script>");   
+            out.println("  document.postSubmit.submit()");   
+            out.println("</script>");   
+        } catch (IOException e) {
+            log.error("post重定向错误", e);
+        } finally {
+            out.close();
+        }
+    }
+}

+ 201 - 0
src/main/java/com/vnpay/demo/controller/DemoPayController.java

@@ -0,0 +1,201 @@
+package com.vnpay.demo.controller;
+
+import com.alibaba.druid.support.json.JSONUtils;
+import com.alibaba.fastjson.JSONObject;
+import com.vnpay.demo.utils.HttpUtil;
+import com.vnpay.demo.utils.MerchantApiUtil;
+import com.vnpay.demo.utils.PayConfigUtil;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 支付Demo控制器
+ * 
+ * @author VnPay Demo
+ */
+@RestController
+@RequestMapping("/demo")
+public class DemoPayController extends BaseController {
+    
+    private static final Log log = LogFactory.getLog(DemoPayController.class);
+    
+    /**
+     * API支付接口
+     * 用于处理支付请求
+     */
+    @PostMapping("/apiPay")
+    public Map<String, Object> apiPay(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            // 获取支付密钥
+            String paySecret = getString_UrlDecode_UTF8("paySecret");
+            if (paySecret == null || paySecret.isEmpty()) {
+                paySecret = PayConfigUtil.readConfig("paySecret");
+            }
+            
+            // 获取请求URL
+            String requestUrl = PayConfigUtil.readConfig("apiPayUrl");
+            if (requestUrl == null || requestUrl.isEmpty()) {
+//                requestUrl = "http://localhost:8080/gateway/api/trade";
+                requestUrl = "https://c.gmobvfxllc.com/gateway/api/trade";
+            }
+            
+            // 构建请求参数
+            Map<String, String> params = new HashMap<String, String>();
+            params.put("tradeType", getString_UrlDecode_UTF8("tradeType"));
+            params.put("version", getString_UrlDecode_UTF8("version"));
+            params.put("channel", getString_UrlDecode_UTF8("channel"));
+            params.put("mchNo", getString_UrlDecode_UTF8("mchNo"));
+            params.put("mchOrderNo", getString_UrlDecode_UTF8("mchOrderNo"));
+            params.put("body", getString_UrlDecode_UTF8("body"));
+            params.put("amount", getString_UrlDecode_UTF8("amount"));
+            params.put("currency", getString_UrlDecode_UTF8("currency"));
+            params.put("timePaid", getString_UrlDecode_UTF8("timePaid"));
+            params.put("remark", getString_UrlDecode_UTF8("remark"));
+            
+            // 构建额外参数
+            Map<String, String> payExtra = new HashMap<String, String>();
+            payExtra.put("notifyUrl", getString_UrlDecode_UTF8("notifyUrl"));
+            payExtra.put("callbackUrl", getString_UrlDecode_UTF8("callbackUrl"));
+            payExtra.put("merchantName", getString_UrlDecode_UTF8("merchantName"));
+            payExtra.put("goodsId", getString_UrlDecode_UTF8("goodsId"));
+            payExtra.put("goodsDesc", getString_UrlDecode_UTF8("goodsDesc"));
+            payExtra.put("showUrl", getString_UrlDecode_UTF8("showUrl"));
+            payExtra.put("memberId", getString_UrlDecode_UTF8("memberId"));
+            payExtra.put("bankType", getString_UrlDecode_UTF8("bankType"));
+            payExtra.put("cardType", getString_UrlDecode_UTF8("cardType"));
+            payExtra.put("orderPeriod", getString_UrlDecode_UTF8("orderPeriod"));
+            
+            // 生成签名
+            Map<String, Object> signMap = new HashMap<String, Object>();
+            signMap.putAll(params);
+            signMap.putAll(payExtra);
+            String sign = MerchantApiUtil.getSign(signMap, paySecret);
+            
+            // 添加额外参数和签名
+            params.put("extra", JSONUtils.toJSONString(payExtra));
+            params.put("sign", sign);
+            
+            // 发送请求
+            log.info("发送支付请求到: " + requestUrl);
+            log.info("请求参数: " + params);
+            String data = HttpUtil.postByHttpClient(requestUrl, params);
+            log.info("响应数据: " + data);
+            
+            // 解析响应
+            Map<String, Object> map = JSONObject.parseObject(data);
+            
+            if (map != null && map.size() > 0) {
+                result.putAll(map);
+                
+                if ((int) map.get("status") != 0) {
+                    result.put("success", false);
+                    result.put("message", map.get("message"));
+                    return result;
+                }
+                
+                if ((int) map.get("resultCode") == 0) {
+                    // 商户在此处处理业务
+                    String codeUrl = (String) map.get("codeUrl");
+                    result.put("success", true);
+                    result.put("codeUrl", codeUrl);
+                    result.put("message", "支付请求成功");
+                }
+            } else {
+                result.put("success", false);
+                result.put("message", "支付网关无响应");
+            }
+            
+        } catch (Exception e) {
+            log.error("支付请求异常", e);
+            result.put("success", false);
+            result.put("message", "支付请求异常: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 异步通知接口
+     * 用于接收支付平台的异步通知
+     */
+    @PostMapping("/notify/getPayInfo")
+    public String notifyUrl(HttpServletRequest request, HttpServletResponse response) {
+        try {
+            // 从配置文件读取支付密钥
+            String paySecret = PayConfigUtil.readConfig("paySecret");
+            if (paySecret == null || paySecret.isEmpty()) {
+                paySecret = "bf183091a6654c339b7b452a996c4ce5";
+            }
+            
+            // 读取请求内容
+            String respString = readRequest(request);
+            log.info("收到异步通知: " + respString);
+            
+            // 解析通知内容
+            Map<String, Object> msgMap = (Map<String, Object>) JSONObject.parse(respString);
+            
+            if (msgMap != null && msgMap.size() != 0) {
+                String sign = (String) msgMap.get("sign");
+                msgMap.remove("sign");
+                
+                // 验证签名
+                String resultSign = MerchantApiUtil.getSign(msgMap, paySecret);
+                
+                if (resultSign.equals(sign)) {
+                    // 签名验证成功,商户在此处处理业务
+                    log.info("签名验证成功,订单号: " + msgMap.get("mchOrderNo"));
+                    
+                    // TODO: 更新订单状态等业务逻辑
+                    
+                    return "success";
+                } else {
+                    log.error("签名验证失败");
+                    return "failed";
+                }
+            }
+            
+        } catch (Exception e) {
+            log.error("处理异步通知异常", e);
+        }
+        
+        return "failed";
+    }
+    
+    /**
+     * 测试接口
+     * 用于测试服务是否正常
+     */
+    @GetMapping("/test")
+    public Map<String, Object> test() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("success", true);
+        result.put("message", "VnPay Demo Service is running!");
+        result.put("time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+        return result;
+    }
+    
+    /**
+     * 首页
+     */
+    @GetMapping("/")
+    public String index() {
+        return "redirect:/index.html";
+    }
+}

+ 307 - 0
src/main/java/com/vnpay/demo/utils/HttpUtil.java

@@ -0,0 +1,307 @@
+package com.vnpay.demo.utils;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.apache.http.util.TextUtils;
+
+/**
+ * HTTP工具类
+ * 用于发送HTTP请求
+ * 
+ * @author VnPay Demo
+ */
+public class HttpUtil {
+
+    public static final int ACITON_POST = 0;
+    public static final int ACITON_GET = 1;
+
+    private static final int IO_BUFFER_SIZE = 4 * 1024;
+    public static final String NET_TYPE_WIFI = "WIFI";
+    public static final String NET_TYPE_MOBILE = "MOBILE";
+    public static final String NET_TYPE_NO_NETWORK = "no_network";
+    public static final int HTTP_REQUEST_TIMEOUT_MS = 60 * 1000;
+    public static final String DEFAULT_CHARSET = "UTF-8";
+    public static final String DEFAULT_CONTENTYPE = "text/html";
+
+    public static String doPostRequest(String url) {
+        return doPostRequest(url, null, null, null);
+    }
+
+    public static String doPostRequest(String url, String parameter) {
+        return doPostRequest(url, parameter, null, null);
+    }
+
+    /**
+     * POST JSON请求
+     */
+    public static String doPostRequest(String url, String parameter, Map<String, String> header, String contentType) {
+        if (TextUtils.isEmpty(url)) {
+            return null;
+        }
+        byte[] data = doRequest(url, parameter, null, null, "POST", contentType, header);
+        try {
+            if (data != null)
+                return new String(data, DEFAULT_CHARSET);
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static String doGetRequest(String url) {
+        return doGetRequest(url, null, null, null);
+    }
+
+    public static String doGetRequest(String url, String parameter, Map<String, String> header, String contentType) {
+        if (TextUtils.isEmpty(url)) {
+            return null;
+        }
+        byte[] data = doRequest(url, parameter, null, null, "GET", contentType, header);
+        try {
+            if (data != null)
+                return new String(data, DEFAULT_CHARSET);
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * HTTP请求
+     */
+    public static byte[] doRequest(String url, String parameter, String userAgent, String charset, String requestMethod,
+            String contentType, Map<String, String> header) {
+        if (TextUtils.isEmpty(url)) {
+            return null;
+        }
+
+        HttpURLConnection connection = null;
+        byte[] exceptionByte = null;
+        try {
+            connection = (HttpURLConnection) getURLConnection(url);
+            connection.setDoInput(true);
+            connection.setDoOutput(true);
+            connection.setRequestMethod(requestMethod);
+            if (header != null) {
+                for (Map.Entry<String, String> entry : header.entrySet()) {
+                    connection.setRequestProperty(entry.getKey(), entry.getValue());
+                }
+            }
+            connection.setRequestProperty("Connection", "Keep-Alive");
+            connection.setRequestProperty("Charset", TextUtils.isEmpty(charset) ? DEFAULT_CHARSET : charset);
+            connection.setRequestProperty("content-type",
+                    TextUtils.isEmpty(contentType) ? DEFAULT_CONTENTYPE : contentType);
+            if (!TextUtils.isEmpty(userAgent))
+                connection.setRequestProperty("User-Agent", userAgent);
+            connection.setConnectTimeout(HTTP_REQUEST_TIMEOUT_MS);
+            connection.setReadTimeout(HTTP_REQUEST_TIMEOUT_MS);
+            connection.connect();
+            InputStream inputStream = null;
+            OutputStream outputStream = null;
+            try {
+                String exception = "exception";
+                exceptionByte = exception.getBytes(DEFAULT_CHARSET);
+                if (!TextUtils.isEmpty(parameter)) {
+                    OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());
+                    out.write(parameter);
+                    out.close();
+                }
+                inputStream = connection.getInputStream();
+                ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
+                outputStream = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE);
+                copy(inputStream, outputStream);
+                outputStream.flush();
+                return dataStream.toByteArray();
+            } finally {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return exceptionByte;
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    protected static URLConnection getURLConnection(String url) {
+        URLConnection connection = null;
+        try {
+            connection = new URL(url).openConnection();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return connection;
+    }
+
+    protected static void copy(InputStream in, OutputStream out) throws IOException {
+        byte[] b = new byte[IO_BUFFER_SIZE];
+        int read;
+        while ((read = in.read(b)) != -1) {
+            out.write(b, 0, read);
+        }
+    }
+
+    public static String getQueryString(HttpServletRequest request) {
+        Map<String, String[]> params = request.getParameterMap();
+        String queryString = "";
+        for (String key : params.keySet()) {
+            String[] values = params.get(key);
+            for (int i = 0; i < values.length; i++) {
+                String value = values[i];
+                queryString += key + "=" + value + "&";
+            }
+        }
+        // 去掉最后一个字符
+        if (!StringUtil.isEmpty(queryString)) {
+            queryString = queryString.substring(0, queryString.length() - 1);
+        }
+        return queryString;
+    }
+
+    /**
+     * 获取完整请求路径
+     */
+    public static String getAddr(HttpServletRequest request) {
+        return request.getRequestURL() + "?" + getQueryString(request);
+    }
+
+    /**
+     * 获取相对请求路径
+     */
+    public static String getRelatedAddr(HttpServletRequest request) {
+        return request.getRequestURI() + "?" + getQueryString(request);
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    public static String getIpAddr(HttpServletRequest request) {
+        String ip = request.getHeader("x-forwarded-for");
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("http_client_ip");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        // 如果是多级代理,那么取第一个ip为客户ip
+        if (ip != null && ip.indexOf(",") != -1) {
+            ip = ip.substring(ip.lastIndexOf(",") + 1, ip.length()).trim();
+        }
+        return ip;
+    }
+
+    /**
+     * 使用HttpClient发送POST请求(表单方式)
+     */
+    public static String postByHttpClient(String url, Map<String, String> params) {
+        try {
+            HttpPost httpPost = new HttpPost(url);
+            CloseableHttpClient client = HttpClients.createDefault();
+            String respContent = null;
+
+            // 表单方式
+            List<BasicNameValuePair> pairList = new ArrayList<BasicNameValuePair>();
+            if(params != null && params.size() > 0) {
+                for(String key: params.keySet()) {
+                    pairList.add(new BasicNameValuePair(key, params.get(key)));
+                }
+            }
+            httpPost.setEntity(new UrlEncodedFormEntity(pairList, "utf-8"));
+
+            HttpResponse resp = client.execute(httpPost);
+            HttpEntity he = resp.getEntity();
+            respContent = EntityUtils.toString(he, "UTF-8");
+            
+            return respContent;
+        } catch(Exception ex) {
+            ex.printStackTrace();
+            return "";
+        }
+    }
+
+    /**
+     * 使用HttpClient发送POST请求(字符串方式)
+     */
+    public static String postByHttpClient(String url, String params) {
+        try {
+            HttpPost httpPost = new HttpPost(url);
+            CloseableHttpClient client = HttpClients.createDefault();
+            String respContent = null;
+
+            StringEntity entityParams = new StringEntity(params,"utf-8");
+            httpPost.setEntity(entityParams);
+            httpPost.setHeader("Content-Type", "text/xml;charset=ISO-8859-1");
+            
+            HttpResponse resp = client.execute(httpPost);
+            if (resp.getStatusLine().getStatusCode() == 200) {
+                HttpEntity he = resp.getEntity();
+                respContent = EntityUtils.toString(he, "UTF-8");
+            }
+            return respContent;
+        } catch(Exception ex) {
+            ex.printStackTrace();
+            return "";
+        }
+    }
+
+    /**
+     * 获取所有请求参数
+     */
+    public static Map<String, String> getAllRequestParam(final HttpServletRequest request) {
+        Map<String, String> res = new HashMap<String, String>();
+        Enumeration<?> temp = request.getParameterNames();
+        if (null != temp) {
+            while (temp.hasMoreElements()) {
+                String en = (String) temp.nextElement();
+                String value = request.getParameter(en);
+                res.put(en, value);
+                // 在报文上送时,如果字段的值为空,则不上送
+                if (null == res.get(en) || "".equals(res.get(en))) {
+                    res.remove(en);
+                }
+            }
+        }
+        return res;
+    }
+}

+ 64 - 0
src/main/java/com/vnpay/demo/utils/MD5Util.java

@@ -0,0 +1,64 @@
+package com.vnpay.demo.utils;
+
+import java.security.MessageDigest;
+
+/**
+ * MD5工具类
+ * 用于生成MD5签名
+ * 
+ * @author VnPay Demo
+ */
+public class MD5Util {
+
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5',
+            '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+    /**
+     * MD5加密
+     * @param origin 原始字符串
+     * @return 加密后的字符串
+     */
+    public static String encode(String origin) {
+        return encode(origin, "UTF-8");
+    }
+
+    /**
+     * MD5加密
+     * @param origin 原始字符串
+     * @param charsetname 字符集
+     * @return 加密后的字符串
+     */
+    public static String encode(String origin, String charsetname) {
+        String resultString = null;
+        try {
+            resultString = new String(origin);
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            if (charsetname == null || "".equals(charsetname)) {
+                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
+            } else {
+                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
+            }
+        } catch (Exception exception) {
+            exception.printStackTrace();
+        }
+        return resultString;
+    }
+
+    private static String byteArrayToHexString(byte b[]) {
+        StringBuffer resultSb = new StringBuffer();
+        for (int i = 0; i < b.length; i++) {
+            resultSb.append(byteToHexString(b[i]));
+        }
+        return resultSb.toString();
+    }
+
+    private static String byteToHexString(byte b) {
+        int n = b;
+        if (n < 0) {
+            n += 256;
+        }
+        int d1 = n / 16;
+        int d2 = n % 16;
+        return "" + HEX_DIGITS[d1] + HEX_DIGITS[d2];
+    }
+}

+ 111 - 0
src/main/java/com/vnpay/demo/utils/MerchantApiUtil.java

@@ -0,0 +1,111 @@
+package com.vnpay.demo.utils;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.*;
+
+/**
+ * 商户API工具类
+ * 用于生成和验证签名
+ * 
+ * @author VnPay Demo
+ */
+public class MerchantApiUtil {
+
+    /**
+     * 获取参数签名
+     * @param paramMap  签名参数
+     * @param paySecret 签名密钥
+     * @return
+     */
+    public static String getSign(Map<String, Object> paramMap, String paySecret) {
+        SortedMap<String, Object> smap = new TreeMap<String, Object>(paramMap);
+        StringBuffer stringBuffer = new StringBuffer();
+        for (Map.Entry<String, Object> m : smap.entrySet()) {
+            Object value = m.getValue();
+            if (value != null && StringUtils.isNotBlank(String.valueOf(value))) {
+                stringBuffer.append(m.getKey()).append("=").append(m.getValue()).append("&");
+            }
+        }
+        stringBuffer.delete(stringBuffer.length() - 1, stringBuffer.length());
+ 
+        String argPreSign = stringBuffer.append("&paySecret=").append(paySecret).toString();
+        System.out.println("签名前字符串: " + argPreSign);
+        String signStr = MD5Util.encode(argPreSign).toUpperCase();
+        System.out.println("签名后: " + signStr);
+
+        return signStr;
+    }
+
+    /**
+     * 获取参数拼接串
+     * @param paramMap
+     * @return
+     */
+    public static String getParamStr(Map<String, Object> paramMap) {
+        SortedMap<String, Object> smap = new TreeMap<String, Object>(paramMap);
+        StringBuffer stringBuffer = new StringBuffer();
+        for (Map.Entry<String, Object> m : smap.entrySet()) {
+            Object value = m.getValue();
+            if (value != null && StringUtils.isNotBlank(String.valueOf(value))) {
+                stringBuffer.append(m.getKey()).append("=").append(value).append("&");
+            }
+        }
+        stringBuffer.delete(stringBuffer.length() - 1, stringBuffer.length());
+
+        return stringBuffer.toString();
+    }
+
+    /**
+     * 验证商户签名
+     * @param paramMap  签名参数
+     * @param paySecret 签名私钥
+     * @param signStr   原始签名密文
+     * @return
+     */
+    public static boolean isRightSign(Map<String, Object> paramMap, String paySecret, String signStr) {
+        if (StringUtils.isBlank(signStr)) {
+            return false;
+        }
+
+        String sign = getSign(paramMap, paySecret);
+        if (signStr.equals(sign)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 建立请求,以表单HTML形式构造(默认)
+     * @param sParaTemp 请求参数数组
+     * @param strMethod 提交方式。两个值可选:post、get
+     * @param strButtonName 确认按钮显示文字
+     * @return 提交表单HTML文本
+     */
+    public static String buildRequest(Map<String, Object> sParaTemp, String strMethod, String strButtonName, String actionUrl) {
+        //待请求参数数组
+        List<String> keys = new ArrayList<String>(sParaTemp.keySet());
+        StringBuffer sbHtml = new StringBuffer();
+
+        sbHtml.append("<form id=\"rppaysubmit\" name=\"rppaysubmit\" action=\"" + actionUrl + "\" method=\"" + strMethod
+                + "\">");
+
+        for (int i = 0; i < keys.size(); i++) {
+            String name = (String) keys.get(i);
+            Object object = sParaTemp.get(name);
+            String value = "";
+
+            if (object != null) {
+                value = (String) sParaTemp.get(name);
+            }
+
+            sbHtml.append("<input type=\"hidden\" name=\"" + name + "\" value=\"" + value + "\"/>");
+        }
+
+        //submit按钮控件请不要含有name属性
+        sbHtml.append("<input type=\"submit\" value=\"" + strButtonName + "\" style=\"display:none;\"></form>");
+
+        return sbHtml.toString();
+    }
+}

+ 68 - 0
src/main/java/com/vnpay/demo/utils/PayConfigUtil.java

@@ -0,0 +1,68 @@
+package com.vnpay.demo.utils;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * 支付配置工具类
+ * 用于读取配置文件
+ * 
+ * @author VnPay Demo
+ */
+public class PayConfigUtil {
+
+    private static final Log LOG = LogFactory.getLog(PayConfigUtil.class);
+
+    /**
+     * 通过静态代码块读取上传文件的验证格式配置文件,静态代码块只执行一次(单例)
+     */
+    private static Properties properties = new Properties();
+
+    private PayConfigUtil() {
+    }
+
+    // 通过类装载器装载进来
+    static {
+        try {
+            // Spring Boot方式读取配置文件
+            Resource resource = new ClassPathResource("application.properties");
+            InputStream inputStream = resource.getInputStream();
+            properties.load(inputStream);
+            inputStream.close();
+        } catch (IOException e) {
+            LOG.error("读取配置文件失败", e);
+            // 如果application.properties不存在,尝试读取pay_config.properties
+            try {
+                properties.load(PayConfigUtil.class.getClassLoader()
+                        .getResourceAsStream("pay_config.properties"));
+            } catch (Exception ex) {
+                LOG.error("读取pay_config.properties失败", ex);
+            }
+        }
+    }
+
+    /**
+     * 读取配置项
+     * @param key 配置键
+     * @return 配置值
+     */
+    public static String readConfig(String key) {
+        return properties.getProperty(key);
+    }
+    
+    /**
+     * 读取配置项,带默认值
+     * @param key 配置键
+     * @param defaultValue 默认值
+     * @return 配置值
+     */
+    public static String readConfig(String key, String defaultValue) {
+        return properties.getProperty(key, defaultValue);
+    }
+}

+ 85 - 0
src/main/java/com/vnpay/demo/utils/StringUtil.java

@@ -0,0 +1,85 @@
+package com.vnpay.demo.utils;
+
+/**
+ * 字符串工具类
+ * 
+ * @author VnPay Demo
+ */
+public class StringUtil {
+
+    /**
+     * 判断字符串是否为空
+     * @param str 字符串
+     * @return true:空 false:非空
+     */
+    public static boolean isEmpty(String str) {
+        return str == null || str.trim().length() == 0;
+    }
+
+    /**
+     * 判断字符串是否非空
+     * @param str 字符串
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(String str) {
+        return !isEmpty(str);
+    }
+
+    /**
+     * 判断字符串是否为空白
+     * @param str 字符串
+     * @return true:空白 false:非空白
+     */
+    public static boolean isBlank(String str) {
+        if (str == null) {
+            return true;
+        }
+        int length = str.length();
+        if (length == 0) {
+            return true;
+        }
+        for (int i = 0; i < length; i++) {
+            if (!Character.isWhitespace(str.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 判断字符串是否非空白
+     * @param str 字符串
+     * @return true:非空白 false:空白
+     */
+    public static boolean isNotBlank(String str) {
+        return !isBlank(str);
+    }
+
+    /**
+     * 去除字符串两端空格
+     * @param str 字符串
+     * @return 去除空格后的字符串
+     */
+    public static String trim(String str) {
+        return str == null ? null : str.trim();
+    }
+
+    /**
+     * 将对象转换为字符串
+     * @param obj 对象
+     * @return 字符串
+     */
+    public static String toString(Object obj) {
+        return obj == null ? "" : obj.toString();
+    }
+
+    /**
+     * 将对象转换为字符串,如果为null则返回默认值
+     * @param obj 对象
+     * @param defaultValue 默认值
+     * @return 字符串
+     */
+    public static String toString(Object obj, String defaultValue) {
+        return obj == null ? defaultValue : obj.toString();
+    }
+}

+ 60 - 0
src/main/resources/application.properties

@@ -0,0 +1,60 @@
+# Spring Boot ??
+server.port=8080
+server.servlet.context-path=/
+spring.application.name=VnPay-Demo
+
+# JSP ??
+spring.mvc.view.prefix=/WEB-INF/jsp/
+spring.mvc.view.suffix=.jsp
+
+# ??????
+spring.mvc.static-path-pattern=/static/**
+spring.resources.static-locations=classpath:/static/
+
+# ????
+server.servlet.encoding.charset=UTF-8
+server.servlet.encoding.enabled=true
+server.servlet.encoding.force=true
+
+# ========== ?????? ==========
+# ????KEY
+payKey=64efebf7eb5b439d8fa213de9028392e
+
+# ????
+paySecret=39ff58d10b904db99259c35d0ea50432
+
+# API??????
+apiPayUrl=https://c.gmobvfxllc.com/gateway/api/trade
+
+# ????????
+scanPayUrl=http://localhost:8080/gateway/scanPay/initPay
+
+# ????????
+f2fPayUrl=http://localhost:8080/gateway/f2fPay/doPay
+
+# ??????????????????????
+notifyUrl=http://your-server.com/demo/notify/getPayInfo
+
+# ??????????????????????
+returnUrl=http://your-server.com/callback
+
+# ??????(??:??)
+orderPeriod=5
+
+# ??IP
+orderIp=192.168.1.13
+
+# ????
+logging.level.root=INFO
+logging.level.com.vnpay.demo=DEBUG
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
+logging.file.name=logs/vnpay-demo.log
+
+# ??????
+spring.devtools.restart.enabled=true
+spring.devtools.restart.additional-paths=src/main/java
+spring.devtools.restart.exclude=static/**,templates/**
+
+# Jackson??
+spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
+spring.jackson.time-zone=GMT+7

+ 281 - 0
src/main/resources/static/index.html

@@ -0,0 +1,281 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>越南支付对接Demo - VnPay Demo</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            max-width: 1200px;
+            margin: 0 auto;
+            padding: 20px;
+            background-color: #f5f5f5;
+        }
+        h1 {
+            color: #333;
+            text-align: center;
+            margin-bottom: 30px;
+        }
+        .section {
+            background: white;
+            padding: 20px;
+            margin-bottom: 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+        }
+        .api-info {
+            background: #e8f4f8;
+            padding: 15px;
+            border-radius: 5px;
+            margin-bottom: 15px;
+        }
+        .code-block {
+            background: #f8f8f8;
+            padding: 15px;
+            border-radius: 5px;
+            overflow-x: auto;
+            font-family: 'Courier New', monospace;
+            font-size: 14px;
+            border: 1px solid #ddd;
+        }
+        .endpoint {
+            color: #0066cc;
+            font-weight: bold;
+        }
+        .method {
+            display: inline-block;
+            padding: 3px 8px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: bold;
+            margin-right: 10px;
+        }
+        .method.post {
+            background: #49cc90;
+            color: white;
+        }
+        .method.get {
+            background: #61affe;
+            color: white;
+        }
+        table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-top: 10px;
+        }
+        th, td {
+            text-align: left;
+            padding: 10px;
+            border-bottom: 1px solid #ddd;
+        }
+        th {
+            background-color: #f0f0f0;
+            font-weight: bold;
+        }
+        .required {
+            color: red;
+            font-weight: bold;
+        }
+    </style>
+</head>
+<body>
+    <h1>越南支付对接Demo - VnPay Demo</h1>
+    
+    <div class="section">
+        <h2>项目信息</h2>
+        <p><strong>项目名称:</strong>VnPay Demo (Spring Boot版本)</p>
+        <p><strong>服务地址:</strong><span id="baseUrl">http://localhost:8080</span></p>
+        <p><strong>当前时间:</strong><span id="currentTime"></span></p>
+        <p><strong>服务状态:</strong><span id="serviceStatus">检查中...</span></p>
+    </div>
+
+    <div class="section">
+        <h2>API接口文档</h2>
+        
+        <div class="api-info">
+            <h3>1. 测试接口</h3>
+            <p><span class="method get">GET</span><span class="endpoint">/demo/test</span></p>
+            <p>用于测试服务是否正常运行</p>
+            <h4>响应示例:</h4>
+            <div class="code-block">
+{
+    "success": true,
+    "message": "VnPay Demo Service is running!",
+    "time": "2024-01-30 20:45:00"
+}
+            </div>
+        </div>
+
+        <div class="api-info">
+            <h3>2. API支付接口</h3>
+            <p><span class="method post">POST</span><span class="endpoint">/demo/apiPay</span></p>
+            <p>用于发起支付请求</p>
+            <h4>请求参数:</h4>
+            <table>
+                <tr>
+                    <th>参数名</th>
+                    <th>类型</th>
+                    <th>必填</th>
+                    <th>说明</th>
+                    <th>示例值</th>
+                </tr>
+                <tr>
+                    <td>tradeType</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>支付类型</td>
+                    <td>SCAN_PAY</td>
+                </tr>
+                <tr>
+                    <td>version</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>版本号</td>
+                    <td>1.0</td>
+                </tr>
+                <tr>
+                    <td>channel</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>支付渠道</td>
+                    <td>ALI_PAY</td>
+                </tr>
+                <tr>
+                    <td>mchNo</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>商户号</td>
+                    <td>M1234567890</td>
+                </tr>
+                <tr>
+                    <td>mchOrderNo</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>商户订单号(唯一)</td>
+                    <td>ORDER20240130001</td>
+                </tr>
+                <tr>
+                    <td>body</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>商品描述</td>
+                    <td>测试商品</td>
+                </tr>
+                <tr>
+                    <td>amount</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>金额(单位:分)</td>
+                    <td>10000</td>
+                </tr>
+                <tr>
+                    <td>currency</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>货币类型</td>
+                    <td>VND</td>
+                </tr>
+                <tr>
+                    <td>timePaid</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>支付时间</td>
+                    <td>20240130120000</td>
+                </tr>
+                <tr>
+                    <td>notifyUrl</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>异步通知地址</td>
+                    <td>http://your-server.com/notify</td>
+                </tr>
+                <tr>
+                    <td>paySecret</td>
+                    <td>String</td>
+                    <td class="required">是</td>
+                    <td>支付密钥(用于签名)</td>
+                    <td>bf183091a6654c339b7b452a996c4ce5</td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="api-info">
+            <h3>3. 异步通知接口</h3>
+            <p><span class="method post">POST</span><span class="endpoint">/demo/notify/getPayInfo</span></p>
+            <p>用于接收支付平台的异步通知</p>
+            <h4>请求体示例(JSON):</h4>
+            <div class="code-block">
+{
+    "mchNo": "M1234567890",
+    "mchOrderNo": "ORDER20240130001",
+    "amount": "10000",
+    "status": "1",
+    "payTime": "20240130120500",
+    "sign": "XXXXXXXXXXXXXXXXXXXXX"
+}
+            </div>
+            <h4>响应:</h4>
+            <p>成功返回: <code>success</code></p>
+            <p>失败返回: <code>failed</code></p>
+        </div>
+    </div>
+
+    <div class="section">
+        <h2>Postman测试示例</h2>
+        <h3>API支付接口测试</h3>
+        <div class="code-block">
+POST http://localhost:8080/demo/apiPay
+Content-Type: application/x-www-form-urlencoded
+
+tradeType=SCAN_PAY&version=1.0&channel=ALI_PAY&mchNo=M1234567890&mchOrderNo=ORDER20240130001&body=测试商品&amount=10000&currency=VND&timePaid=20240130120000&remark=测试备注&notifyUrl=http://your-server.com/notify&callbackUrl=http://your-server.com/callback&merchantName=测试商户&goodsId=GOODS001&goodsDesc=测试商品描述&paySecret=bf183091a6654c339b7b452a996c4ce5
+        </div>
+    </div>
+
+    <div class="section">
+        <h2>签名规则说明</h2>
+        <ol>
+            <li>将所有参数按照参数名ASCII码从小到大排序</li>
+            <li>拼接成key1=value1&key2=value2格式</li>
+            <li>在末尾拼接&paySecret=密钥值</li>
+            <li>对整个字符串进行MD5加密并转为大写</li>
+        </ol>
+        <p><strong>示例:</strong></p>
+        <div class="code-block">
+原始参数: amount=10000&body=测试商品&channel=ALI_PAY...
+拼接密钥: amount=10000&body=测试商品&channel=ALI_PAY...&paySecret=bf183091a6654c339b7b452a996c4ce5
+MD5加密: MD5(拼接后的字符串).toUpperCase()
+        </div>
+    </div>
+
+    <script>
+        // 显示当前时间
+        function updateTime() {
+            const now = new Date();
+            document.getElementById('currentTime').textContent = now.toLocaleString('zh-CN');
+        }
+        updateTime();
+        setInterval(updateTime, 1000);
+
+        // 检查服务状态
+        fetch('/demo/test')
+            .then(response => response.json())
+            .then(data => {
+                if (data.success) {
+                    document.getElementById('serviceStatus').textContent = '运行正常 ✓';
+                    document.getElementById('serviceStatus').style.color = 'green';
+                } else {
+                    document.getElementById('serviceStatus').textContent = '服务异常 ✗';
+                    document.getElementById('serviceStatus').style.color = 'red';
+                }
+            })
+            .catch(error => {
+                document.getElementById('serviceStatus').textContent = '无法连接 ✗';
+                document.getElementById('serviceStatus').style.color = 'red';
+            });
+
+        // 更新基础URL
+        document.getElementById('baseUrl').textContent = window.location.origin;
+    </script>
+</body>
+</html>

+ 44 - 0
start.bat

@@ -0,0 +1,44 @@
+@echo off
+echo ====================================
+echo 越南支付对接Demo - Spring Boot启动脚本
+echo ====================================
+echo.
+
+echo 正在检查Java环境...
+java -version >nul 2>&1
+if errorlevel 1 (
+    echo 错误: 未找到Java环境,请先安装JDK 1.8+
+    pause
+    exit /b 1
+)
+
+echo 正在检查Maven环境...
+mvn -version >nul 2>&1
+if errorlevel 1 (
+    echo 错误: 未找到Maven环境,请先安装Maven 3.x+
+    pause
+    exit /b 1
+)
+
+echo.
+echo 正在编译项目...
+call mvn clean package -DskipTests
+
+if errorlevel 1 (
+    echo 编译失败,请检查错误信息
+    pause
+    exit /b 1
+)
+
+echo.
+echo 正在启动项目...
+echo.
+echo ====================================
+echo 项目启动成功后,请访问:
+echo http://localhost:8080
+echo ====================================
+echo.
+
+java -jar target/springboot-vnpay-demo-1.0.0.jar
+
+pause

+ 38 - 0
start.sh

@@ -0,0 +1,38 @@
+#!/bin/bash
+
+echo "===================================="
+echo "越南支付对接Demo - Spring Boot启动脚本"
+echo "===================================="
+echo
+
+echo "正在检查Java环境..."
+if ! command -v java &> /dev/null; then
+    echo "错误: 未找到Java环境,请先安装JDK 1.8+"
+    exit 1
+fi
+
+echo "正在检查Maven环境..."
+if ! command -v mvn &> /dev/null; then
+    echo "错误: 未找到Maven环境,请先安装Maven 3.x+"
+    exit 1
+fi
+
+echo
+echo "正在编译项目..."
+mvn clean package -DskipTests
+
+if [ $? -ne 0 ]; then
+    echo "编译失败,请检查错误信息"
+    exit 1
+fi
+
+echo
+echo "正在启动项目..."
+echo
+echo "===================================="
+echo "项目启动成功后,请访问:"
+echo "http://localhost:8080"
+echo "===================================="
+echo
+
+java -jar target/springboot-vnpay-demo-1.0.0.jar