feat:邮储支付
This commit is contained in:
parent
b91923c44f
commit
a2202ea200
443
README.md
443
README.md
|
|
@ -1,74 +1,393 @@
|
|||
## Snow
|
||||
Snow是一套简单易用的Go语言业务框架,整体逻辑设计简洁,支持HTTP服务、队列调度和任务调度等常用业务场景模式。
|
||||
# PaymentCenter 支付中心
|
||||
|
||||
## Quick start
|
||||
一个基于 Go + Gin 的统一支付网关系统,为商户提供下单、退款、查询、收银台等支付能力,底层对接微信支付、支付宝等第三方支付平台。
|
||||
|
||||
### Build
|
||||
sh build/shell/build.sh
|
||||
> 代码位置:后端接口位于 `app/`,收银台页面模板位于 `front/templates/`。
|
||||
|
||||
### Run
|
||||
```shell
|
||||
1. build/bin/snow -a api #启动Api服务
|
||||
2. build/bin/snow -a cron #启动Cron定时任务服务
|
||||
3. build/bin/snow -a job #启动队列调度服务
|
||||
4. build/bin/snow -a command -m test #执行名称为test的脚本任务
|
||||
---
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
| 组件 | 选型 |
|
||||
|------|------|
|
||||
| 语言 | Go |
|
||||
| Web 框架 | [Gin](https://github.com/gin-gonic/gin) |
|
||||
| ORM | [xorm](https://xorm.io/) |
|
||||
| 第三方支付 | [go-pay](https://github.com/go-pay/gopay) |
|
||||
| JSON | [bytedance/sonic](https://github.com/bytedance/sonic) |
|
||||
| 参数校验 | go-playground/validator.v9 |
|
||||
| 缓存/分布式锁 | Redis(snow-core/redis) |
|
||||
| 数据库 | MySQL |
|
||||
| 加密算法 | RSA / SM2 / SM4 / AES / DES3 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目录结构
|
||||
|
||||
```
|
||||
PaymentCenter/
|
||||
├── app/
|
||||
│ ├── constants/ # 常量定义(错误码、公共枚举、pojo)
|
||||
│ │ ├── common/common.go # 订单状态、支付渠道、路由前缀
|
||||
│ │ └── errorcode/ # 错误码与中文消息
|
||||
│ ├── http/
|
||||
│ │ ├── routes/route.go # 路由注册入口
|
||||
│ │ ├── requestmapping/ # 路由→请求结构体映射
|
||||
│ │ ├── middlewares/ # 中间件(CORS、鉴权、加解密、参数校验)
|
||||
│ │ ├── controllers/
|
||||
│ │ │ ├── base.go # 公共:Success/Error/GenRequest/ApiRes
|
||||
│ │ │ ├── front/ # C 端:支付 API、收银台、回调
|
||||
│ │ │ └── backend/ # 管理后台
|
||||
│ │ └── entities/ # 请求/响应结构体
|
||||
│ ├── services/
|
||||
│ │ ├── api_request_valid.go # App 校验 & IP 白名单
|
||||
│ │ ├── order.go # 订单 CRUD & 状态检查
|
||||
│ │ ├── pay_page.go # 收银台渠道列表、订单状态检查
|
||||
│ │ ├── apicrypt/ # RSA/SM2/SM4 加解密实现
|
||||
│ │ └── thirdpay/ # 第三方支付业务封装
|
||||
│ │ ├── pay.go # 下单/退款/关闭主流程
|
||||
│ │ ├── pay_page.go # 收银台业务:V2 预下单、查询
|
||||
│ │ ├── pay_query.go # 三方查询
|
||||
│ │ ├── do/ # 渠道策略层(PayCheck、Pay、PayWay 映射)
|
||||
│ │ └── thirdpay_notify/ # 回调异步通知处理
|
||||
│ ├── third/paymentService/ # go-pay 封装(微信/支付宝底层调用)
|
||||
│ ├── models/ # xorm 实体(orders、app、merchant、pay_channel…)
|
||||
│ ├── data/ # Repository 层(OrderRepo、PayChannelRepo 等)
|
||||
│ ├── utils/ # snowflake、JWT、HTTP client、useragent、DES
|
||||
│ │ └── encrypt/ # AES、RSA、SM2、SM4
|
||||
│ ├── caches/ # 缓存
|
||||
│ └── mq/ # 消息队列
|
||||
└── front/
|
||||
└── templates/
|
||||
├── payPage.html # 收银台主页面
|
||||
├── payTemplateDefault.html # 渠道跳转模板
|
||||
├── success.html # 支付成功(JSAPI 场景)
|
||||
└── fail.html # 支付失败
|
||||
```
|
||||
|
||||
## Documents
|
||||
---
|
||||
|
||||
- [项目地址](https://github.com/qit-team/snow)
|
||||
- [中文文档](https://github.com/qit-team/snow/wiki)
|
||||
- [changelog](https://github.com/qit-team/snow/blob/master/CHANGLOG.md)
|
||||
- [xorm](http://gobook.io/read/github.com/go-xorm/manual-zh-CN/)
|
||||
## 3. 架构分层与调用链
|
||||
|
||||
##
|
||||
<!--
|
||||
```
|
||||
Gin 路由
|
||||
│
|
||||
▼
|
||||
Middleware(CORS → ValidateRequest / ValidatePayRequest → 加解密)
|
||||
│
|
||||
▼
|
||||
Controller(front/ 或 backend/)
|
||||
│
|
||||
▼
|
||||
services/(业务编排 + services/thirdpay/)
|
||||
│
|
||||
▼
|
||||
services/thirdpay/do/(PayCheck → Pay → PayWay 策略)
|
||||
│
|
||||
▼
|
||||
third/paymentService/(go-pay 底层调用)→ 微信 / 支付宝
|
||||
│
|
||||
▼
|
||||
data/(Repository)→ models/(xorm ORM)→ MySQL
|
||||
```
|
||||
|
||||
// validator的示例
|
||||
// HandleTestValidator godoc
|
||||
// @Summary HandleTestValidator的示例
|
||||
// @Description HandleTestValidator的示例
|
||||
// @Tags snow
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param testValidator body entities.TestValidatorRequest true "example of validator"
|
||||
// @Success 200 {array} entities.TestValidatorRequest
|
||||
// @Failure 400 {object} controllers.HTTPError
|
||||
// @Failure 404 {object} controllers.HTTPError
|
||||
// @Failure 500 {object} controllers.HTTPError
|
||||
// @Router /test_validator [post]
|
||||
func HandleTestValidator(c *gin.Context) {
|
||||
request := new(entities.TestValidatorRequest)
|
||||
err := GenRequest(c, request)
|
||||
if err != nil {
|
||||
Error(c, errorcode.ParamError)
|
||||
return
|
||||
- **控制器层**:只做 HTTP 层的事(绑定请求、调用 service、封装返回)
|
||||
- **服务层**:核心业务(状态机、幂等、分布式锁、渠道路由)
|
||||
- **Repository(data/)**:只做数据库 CRUD,不包含业务逻辑
|
||||
|
||||
---
|
||||
|
||||
## 4. C 端接口清单
|
||||
|
||||
> 接口前缀:`/pay/front/api/v1` 或 `/pay/front/api/v2`
|
||||
|
||||
| 方法 | 路由 | 中间件 | 功能 |
|
||||
|------|------|--------|------|
|
||||
| POST | `/notify/wx/:payChannelId` | - | 微信支付回调 |
|
||||
| POST | `/notify/ali/:payChannelId` | - | 支付宝支付回调 |
|
||||
| POST | `/pay/url` (V1) | ValidatePayRequest | 下单,同步返回支付 URL / JsInfo |
|
||||
| POST | `/pay/url` (V2) | ValidatePayRequest | 预下单,仅创建订单,返回收银台 URL |
|
||||
| POST | `/pay/refund` | ValidatePayRequest | 退款 |
|
||||
| POST | `/pay/query` | ValidatePayRequest | 查询订单状态 |
|
||||
| POST | `/pay/close` | ValidatePayRequest | 关闭订单 |
|
||||
| GET | `/payPage?no={id}` | - | 收银台 HTML 页面入口 |
|
||||
| POST | `/payPage/list` | ValidateRequest | 获取可用支付渠道列表 |
|
||||
| GET | `/payPage/submit` | ValidateRequest | 获取支付跳转链接(渲染 payTemplateDefault.html)|
|
||||
| POST | `/payPage/query` | - | 收银台页面轮询订单状态 |
|
||||
| POST | `/ali/getOauth` | ValidateRequest | 支付宝小程序获取 openId |
|
||||
| POST/GET | `/wx/*` | ValidateRequest | 微信授权 / openId / JSAPI / 小程序支付 |
|
||||
|
||||
- 路由定义参见 [app/http/routes/route.go](file:///e:/goproject/goproject/PaymentCenter/app/http/routes/route.go)
|
||||
- 路由→结构体映射参见 [app/http/requestmapping/front.go](file:///e:/goproject/goproject/PaymentCenter/app/http/requestmapping/front.go)
|
||||
|
||||
---
|
||||
|
||||
## 5. 两种下单模式
|
||||
|
||||
### 5.1 模式一:V1 直接下单(`POST /pay/front/api/v1/pay/url`)
|
||||
|
||||
适用于商户自己管理收银台页面:
|
||||
|
||||
1. 商户构造请求 `RequestBody{ app_id, timestamp, data, key }`,加密后发起
|
||||
2. 中间件 `ValidatePayRequest()`:查 App 配置 → 解密 → 反序列化为 `PayReqs`
|
||||
3. 控制器调用 `thirdpay.ThirdPayInfoCheck()` → `thirdpay.ThirdPayUrl()`
|
||||
4. 同步返回 `ApiResponse{ Order, Url, JsInfo }`
|
||||
|
||||
### 5.2 模式二:V2 预下单 + 收银台(推荐)
|
||||
|
||||
适用于商户跳转至我方收银台页面:
|
||||
|
||||
1. 商户调用 `POST /pay/front/api/v2/pay/url`
|
||||
2. `PayUrlV2Service()`:
|
||||
- Redis 分布式锁(`payUrl:{out_trade_no}`,3 秒)
|
||||
- 检查商户是否有可用支付渠道
|
||||
- 按 `out_trade_no + app_id` 查询订单;不存在则新建订单(状态 WAITPAY)
|
||||
- 返回收银台 URL:`https://pay-host/pay/front/api/v1/payPage?no={orders.id}`
|
||||
3. 商户将用户浏览器跳转到收银台
|
||||
4. **收银台页面**(`payPage.html`):
|
||||
- 读取 URL 参数 `no`(**订单自增 ID,不是 out_trade_no**)
|
||||
- `POST /payPage/list?id=xxx` 拉取渠道列表
|
||||
- 单渠道直接跳转 `/payPage/submit`,多渠道让用户选择
|
||||
- 选择后跳转到第三方支付页面
|
||||
- 支付完成后轮询 `/payPage/query`,成功后跳转到商户 `return_url`
|
||||
|
||||
---
|
||||
|
||||
## 6. 收银台页面流程
|
||||
|
||||
```
|
||||
商户后台
|
||||
│ POST /v2/pay/url ─────┐
|
||||
│ ▼
|
||||
│ 支付中心创建订单
|
||||
│ │
|
||||
│ ▼
|
||||
│ 返回 payPage?no={orderId}
|
||||
│
|
||||
└── 用户浏览器 ───► GET payPage.html
|
||||
│
|
||||
├── JS 调 POST /payPage/list 获取渠道
|
||||
│
|
||||
├── GET /payPage/submit 获取跳转链接
|
||||
│ └──► payTemplateDefault.html window.location
|
||||
│
|
||||
├── POST /payPage/query 轮询订单状态
|
||||
│
|
||||
└── 支付成功 → 跳转 return_url
|
||||
```
|
||||
|
||||
页面文件:
|
||||
|
||||
- [payPage.html](file:///e:/goproject/goproject/PaymentCenter/front/templates/payPage.html) — 收银台主页(含加载态、渠道选择、状态确认弹窗)
|
||||
- [payTemplateDefault.html](file:///e:/goproject/goproject/PaymentCenter/front/templates/payTemplateDefault.html) — 第三方支付跳转
|
||||
- [success.html](file:///e:/goproject/goproject/PaymentCenter/front/templates/success.html) — 微信 JSAPI 成功
|
||||
- [fail.html](file:///e:/goproject/goproject/PaymentCenter/front/templates/fail.html) — 支付失败
|
||||
|
||||
前端注意:`payPage.html` 中的 `API_BASE_URL` 默认写死为 `http://localhost:7081`,部署时需要替换为生产地址。
|
||||
|
||||
---
|
||||
|
||||
## 7. 请求加解密体系
|
||||
|
||||
### 7.1 两个核心中间件
|
||||
|
||||
| 中间件 | 路由 | 说明 |
|
||||
|--------|------|------|
|
||||
| `ValidateRequest()` | 收银台列表、授权、小程序等 | 普通参数解析(form/json)+ validator 校验 + 可选日志 |
|
||||
| `ValidatePayRequest()` | `/pay/url`、`/pay/refund`、`/pay/query`、`/pay/close` | 支付专用:`app_id` 查表 → 按 `key_type` 解密 data → 二次解析校验 |
|
||||
|
||||
中间件代码:[app/http/middlewares/base.go](file:///e:/goproject/goproject/PaymentCenter/app/http/middlewares/base.go)
|
||||
|
||||
### 7.2 支付请求结构(加密封装)
|
||||
|
||||
```
|
||||
{
|
||||
"app_id": 123, // 应用 ID,查表得 KeyType
|
||||
"timestamp": 1700000000, // 时间戳
|
||||
"data": "...", // 真实业务参数的密文(PayReqs JSON → 加密 → Base64)
|
||||
"key": "..." // 部分算法所需对称密钥密文
|
||||
}
|
||||
```
|
||||
|
||||
Success(c, request)
|
||||
return
|
||||
}
|
||||
解密后 `data` 对应的业务结构,在 `requestmapping.FrontRequestMap` 中按路由映射。
|
||||
|
||||
// Address houses a users address information
|
||||
type Address struct {
|
||||
Street string `json:"street" validate:"required" example:"huandaodonglu"`
|
||||
City string `json:"city" validate:"required" example:"xiamen"`
|
||||
Planet string `json:"planet" validate:"required" example:"snow"`
|
||||
Phone string `json:"phone" validate:"required" example:"snow"`
|
||||
}
|
||||
### 7.3 加密算法(由 `app.key_type` 决定)
|
||||
|
||||
// 请求数据结构
|
||||
type TestValidatorRequest struct {
|
||||
//tips,因为组件required不管是没传值或者传 0 or "" 都通过不了,但是如果用指针类型,那么0就是0,而nil无法通过校验
|
||||
Id *int64 `json:"id" validate:"required" example:"1"`
|
||||
Age int `json:"age" validate:"required,gte=0,lte=130" example:"20"`
|
||||
Name *string `json:"name" validate:"required" example:"snow"`
|
||||
Email string `json:"email" validate:"required,email" example:"snow@github.com"`
|
||||
Url string `json:"url" validate:"required" example:"github.com/qit-team/snow"`
|
||||
Mobile string `json:"mobile" validate:"required" example:"snow"`
|
||||
RangeNum int `json:"range_num" validate:"max=10,min=1" example:"3"`
|
||||
TestNum *int `json:"test_num" validate:"required,oneof=5 7 9" example:"7"`
|
||||
Content *string `json:"content" example:"snow"`
|
||||
Addresses []*Address `json:"addresses" validate:"required,dive,required" `
|
||||
}
|
||||
-->
|
||||
| 类型 | 实现文件 |
|
||||
|------|---------|
|
||||
| NO_CRYPT | 明文(不加密) |
|
||||
| RSA | [services/apicrypt/rsa.go](file:///e:/goproject/goproject/PaymentCenter/app/services/apicrypt/rsa.go) |
|
||||
| SM2 | [services/apicrypt/sm2.go](file:///e:/goproject/goproject/PaymentCenter/app/services/apicrypt/sm2.go) |
|
||||
| SM4 | [services/apicrypt/sm4.go](file:///e:/goproject/goproject/PaymentCenter/app/services/apicrypt/sm4.go) |
|
||||
|
||||
注册处:[services/apicrypt/types.go](file:///e:/goproject/goproject/PaymentCenter/app/services/apicrypt/types.go) 中的 `ApiCryptMap`
|
||||
|
||||
### 7.4 响应加密
|
||||
|
||||
- 非生产环境:`controllers.Success/Error` 直接返回 `{ code, message, data }` JSON
|
||||
- 生产环境(`config.GetConf().Env == "production"`):整体 JSON 用 DES3 + Base64 加密后以纯文本返回
|
||||
- 对应方法:`controllers.EncriptJson()`
|
||||
|
||||
---
|
||||
|
||||
## 8. 订单模型 & 状态机
|
||||
|
||||
### 8.1 Orders 表
|
||||
|
||||
字段定义:[app/models/ordersmodel/orders.go](file:///e:/goproject/goproject/PaymentCenter/app/models/ordersmodel/orders.go)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int64 | 主键,snowflake 生成 |
|
||||
| merchant_id | int64 | 商户 |
|
||||
| pay_channel_id | int64 | 支付渠道实例 |
|
||||
| app_id | int64 | 应用 |
|
||||
| out_trade_no | string(50) | 外部/商户订单号 |
|
||||
| order_type | tinyint | 1=支付, 2=退款 |
|
||||
| refund_order_id | int64 | 退款关联的原订单 |
|
||||
| amount | int | 订单金额,单位:**分** |
|
||||
| actual_amount | int | 实际金额,单位:**分** |
|
||||
| payer_total | int | 付款方金额,单位:**分** |
|
||||
| ext_json | string(1024) | 扩展 JSON |
|
||||
| desc | string(100) | 商品描述 |
|
||||
| status | tinyint | 订单状态(见下) |
|
||||
| create_time / update_time | datetime | 自动维护 |
|
||||
|
||||
### 8.2 订单状态(`constants/common/common.go`)
|
||||
|
||||
| 常量 | 值 | 说明 |
|
||||
|------|----|------|
|
||||
| ORDER_STATUS_WAITPAY | 1 | 待支付(刚创建) |
|
||||
| ORDER_STATUS_PAYING | 2 | 支付中(已调第三方下单) |
|
||||
| ORDER_STATUS_PAYED | 3 | 支付成功 |
|
||||
| ORDER_STATUS_FAILED | 4 | 支付失败 |
|
||||
| ORDER_STATUS_CLOSE | 5 | 已关闭 |
|
||||
|
||||
### 8.3 状态流转
|
||||
|
||||
```
|
||||
WAITPAY(1) ──► PAYING(2) ──┬──► PAYED(3)
|
||||
│ │
|
||||
└─────► CLOSE(5) └──► FAILED(4)
|
||||
```
|
||||
|
||||
状态检查方法:`services.OrderStatusCheck(order)`,位置 [app/services/pay_page.go](file:///e:/goproject/goproject/PaymentCenter/app/services/pay_page.go#L89-L103)
|
||||
|
||||
---
|
||||
|
||||
## 9. 支付渠道体系
|
||||
|
||||
### 9.1 渠道类型枚举
|
||||
|
||||
定义在 `constants/common/common.go`:
|
||||
|
||||
| 常量 | 值 | 含义 |
|
||||
|------|----|------|
|
||||
| PAY_CHANNEL_WECHAT_JSAPI | 1 | 微信公众号 JSAPI |
|
||||
| PAY_CHANNEL_WECHAT_H5 | 2 | 微信 H5 |
|
||||
| PAY_CHANNEL_WECHAT_APP | 3 | 微信 APP |
|
||||
| PAY_CHANNEL_WECHAT_NATIVE | 4 | 微信 Native 扫码 |
|
||||
| PAY_CHANNEL_WECHAT_MINI | 5 | 微信小程序 |
|
||||
| PAY_CHANNEL_ALIPAY_WEB | 6 | 支付宝网页/移动应用 |
|
||||
| PAY_CHANNEL_ALIPAY_MINI | 7 | 支付宝小程序 |
|
||||
| PAY_CHANNEL_ALIPAY_JSAPI | 8 | 支付宝 JSAPI |
|
||||
| PAY_CHANNEL_ALIPAY_PC | 9 | 支付宝 PC 网站 |
|
||||
|
||||
### 9.2 策略映射(渠道 → 实现方法)
|
||||
|
||||
- 下单:`services/thirdpay/do/pay_way.go` 的 `PayWayList[channelType]`
|
||||
- 退款:`services/thirdpay/do/refund_way.go` 的 `RefundWayList[channelType]`
|
||||
- 底层统一入口:`third/paymentService/payment_service.go` 的 `PaymentService(ctx, req)`
|
||||
- 根据 `req.ChannelType` 路由到 `AliPayService()` 或 `WechatPayService()`
|
||||
|
||||
### 9.3 客户端环境 → 可用渠道
|
||||
|
||||
- 微信浏览器内 → JSAPI(需要 openId,收银台会自动先走授权)
|
||||
- PC 浏览器 → 微信 Native、支付宝 PC
|
||||
- 手机浏览器(非微信内)→ 微信 H5、支付宝 Web
|
||||
|
||||
实现:`services.PayPageChannelList()` + `services.ClientEnvCheck(ua)`
|
||||
|
||||
---
|
||||
|
||||
## 10. 新增功能的操作清单
|
||||
|
||||
### 10.1 新增一个 C 端 API
|
||||
|
||||
1. 在 `app/http/entities/front/` 定义 Request/Response 结构体
|
||||
2. 在 `app/http/requestmapping/front.go` 的两个 map 中添加路由 → 结构体映射
|
||||
3. 在 `app/http/routes/route.go` 注册路由,挂上合适的中间件
|
||||
4. 在 `app/http/controllers/front/` 对应文件新增 handler(用 `controllers.GetRequest(c).(*front.XXX)` 取参数)
|
||||
5. 在 `app/services/` 或 `app/services/thirdpay/` 实现业务逻辑
|
||||
|
||||
### 10.2 新增一个支付渠道
|
||||
|
||||
1. 在 `constants/common/common.go` 增加 `PAY_CHANNEL_XXX` 常量和 `PayChannelName`/`PayChannelList` 条目
|
||||
2. 在 `services/thirdpay/do/pay_way.go` 的 `PayWayList` 注册实现方法
|
||||
3. 在 `third/paymentService/wechat_service.go` 或 `ali_service.go` 增加对应分支
|
||||
4. 若需区分客户端环境,更新 `OpenInPayChannelMap`
|
||||
|
||||
### 10.3 新增一个页面模板
|
||||
|
||||
1. 在 `front/templates/` 下新增 `.html`
|
||||
2. 路由里通过 `c.HTML(http.StatusOK, "文件名.html", gin.H{...})` 渲染
|
||||
3. `router.LoadHTMLGlob("./front/templates/*")` 已在 routes 中注册
|
||||
|
||||
### 10.4 新增一个错误码
|
||||
|
||||
1. 在 `constants/errorcode/error_code.go` 定义 const,并在 `ErrCodeMap` 注册中文消息
|
||||
2. 使用:`controllers.Error(c, errorcode.XXX)` 或 `controllers.ApiRes(c, nil, code)`
|
||||
|
||||
### 10.5 新增数据模型 / Repository
|
||||
|
||||
1. 在 `app/models/xxxmodel/` 定义结构体(xorm tag + TableName + GetInstance 单例)
|
||||
2. 在 `app/data/` 新增 `XxxRepo` 封装 xorm 操作(推荐配合 `builder.Cond` 拼装条件)
|
||||
3. 在 `app/services/` 新增 `XxxFindOne` / `XxxList` 等服务方法
|
||||
|
||||
---
|
||||
|
||||
## 11. 编码规范与注意事项
|
||||
|
||||
- **金额单位**:全链路使用 **分**(int);前端展示时 `/100` 转元;不得出现 float64 金额
|
||||
- **订单 ID vs out_trade_no**:
|
||||
- `orders.id` = 雪花自增 ID(支付中心内部、收银台查询使用)
|
||||
- `out_trade_no` = 商户外部订单号(商户下单、查询使用)
|
||||
- **不要混用**
|
||||
- **幂等性**:V2 下单通过 Redis 分布式锁(key=`payUrl:{out_trade_no}`,3 秒)+ 数据库 `(out_trade_no, app_id)` 唯一约束保证
|
||||
- **中间件选型**:
|
||||
- 加密支付接口 → `ValidatePayRequest()`
|
||||
- 收银台、授权、小程序等 → `ValidateRequest()`
|
||||
- 回调接口 → 直接在 controller 解析(走第三方验签)
|
||||
- **统一返回**:`controllers.Success/Error/ApiRes/HandCodeRes`,不要手写 JSON
|
||||
- **错误码来源**:`constants/errorcode/error_code.go`,不要硬编码数字
|
||||
- **日志**:`utils.Log(ctx, tag, ...)`;支付链路关键节点必须有日志
|
||||
- **单元测试**:工具类、加解密、snowflake 等均有 `_test.go`,新工具函数建议配套
|
||||
|
||||
---
|
||||
|
||||
## 12. 关键文件速查
|
||||
|
||||
| 功能 | 文件 |
|
||||
|------|------|
|
||||
| 路由注册 | [app/http/routes/route.go](file:///e:/goproject/goproject/PaymentCenter/app/http/routes/route.go) |
|
||||
| 路由→结构体映射 | [app/http/requestmapping/front.go](file:///e:/goproject/goproject/PaymentCenter/app/http/requestmapping/front.go) |
|
||||
| 中间件 | [app/http/middlewares/base.go](file:///e:/goproject/goproject/PaymentCenter/app/http/middlewares/base.go) |
|
||||
| 公共返回/参数解析 | [app/http/controllers/base.go](file:///e:/goproject/goproject/PaymentCenter/app/http/controllers/base.go) |
|
||||
| 支付接口 controller | [app/http/controllers/front/api.go](file:///e:/goproject/goproject/PaymentCenter/app/http/controllers/front/api.go) |
|
||||
| 收银台 controller | [app/http/controllers/front/pay_page.go](file:///e:/goproject/goproject/PaymentCenter/app/http/controllers/front/pay_page.go) |
|
||||
| 回调/微信/支付宝 controller | [app/http/controllers/front/payment_controller.go](file:///e:/goproject/goproject/PaymentCenter/app/http/controllers/front/payment_controller.go) |
|
||||
| 请求结构体 | [app/http/entities/front/pay.go](file:///e:/goproject/goproject/PaymentCenter/app/http/entities/front/pay.go) |
|
||||
| 订单服务 | [app/services/order.go](file:///e:/goproject/goproject/PaymentCenter/app/services/order.go) |
|
||||
| 收银台服务(渠道列表/状态检查) | [app/services/pay_page.go](file:///e:/goproject/goproject/PaymentCenter/app/services/pay_page.go) |
|
||||
| App 校验 | [app/services/api_request_valid.go](file:///e:/goproject/goproject/PaymentCenter/app/services/api_request_valid.go) |
|
||||
| 第三方支付主流程 | [app/services/thirdpay/pay.go](file:///e:/goproject/goproject/PaymentCenter/app/services/thirdpay/pay.go) |
|
||||
| 收银台/V2 下单 | [app/services/thirdpay/pay_page.go](file:///e:/goproject/goproject/PaymentCenter/app/services/thirdpay/pay_page.go) |
|
||||
| 加解密类型注册 | [app/services/apicrypt/types.go](file:///e:/goproject/goproject/PaymentCenter/app/services/apicrypt/types.go) |
|
||||
| 错误码 | [app/constants/errorcode/error_code.go](file:///e:/goproject/goproject/PaymentCenter/app/constants/errorcode/error_code.go) |
|
||||
| 公共常量 | [app/constants/common/common.go](file:///e:/goproject/goproject/PaymentCenter/app/constants/common/common.go) |
|
||||
| Orders 模型 | [app/models/ordersmodel/orders.go](file:///e:/goproject/goproject/PaymentCenter/app/models/ordersmodel/orders.go) |
|
||||
| Order Repository | [app/data/orders.go](file:///e:/goproject/goproject/PaymentCenter/app/data/orders.go) |
|
||||
| 底层 go-pay 调用入口 | app/third/paymentService/payment_service.go |
|
||||
| 收银台页面 | [front/templates/payPage.html](file:///e:/goproject/goproject/PaymentCenter/front/templates/payPage.html) |
|
||||
| 跳转模板 | [front/templates/payTemplateDefault.html](file:///e:/goproject/goproject/PaymentCenter/front/templates/payTemplateDefault.html) |
|
||||
| 雪花 ID 生成 | [app/utils/snowflake/snow_flake.go](file:///e:/goproject/goproject/PaymentCenter/app/utils/snowflake/snow_flake.go) |
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const (
|
|||
PAY_CHANNEL_ALIPAY_MINI = 7
|
||||
PAY_CHANNEL_ALIPAY_JSAPI = 8
|
||||
PAY_CHANNEL_ALIPAY_PC = 9
|
||||
PAY_CHANNEL_PSBC = 11
|
||||
|
||||
// 统一登陆信息
|
||||
ADMIN_USER_ID = "User-Id"
|
||||
|
|
@ -43,6 +44,7 @@ const (
|
|||
|
||||
PAY_CHANNLE_TYPE_WECHAT = 1 // 支付类型: 微信
|
||||
PAY_CHANNLE_TYPE_ZFB = 2 // 支付类型:支付宝
|
||||
PAY_CHANNLE_TYPE_PSBC = 3 // 支付类型:邮储
|
||||
|
||||
THIRD_ORDER_TYPE_PAY = 1 // 发起支付
|
||||
THIRD_ORDER_TYPE_ORDER_QUERY = 2 // 查询订单
|
||||
|
|
@ -66,6 +68,7 @@ var PayChannelName = map[int]string{
|
|||
PAY_CHANNEL_ALIPAY_MINI: "支付宝支付",
|
||||
PAY_CHANNEL_ALIPAY_JSAPI: "支付宝支付",
|
||||
PAY_CHANNEL_ALIPAY_PC: "支付宝支付",
|
||||
PAY_CHANNEL_PSBC: "邮储支付",
|
||||
}
|
||||
|
||||
var PayChannelList = map[int]string{
|
||||
|
|
@ -77,6 +80,7 @@ var PayChannelList = map[int]string{
|
|||
PAY_CHANNEL_ALIPAY_WEB: "支付宝网页&移动应用",
|
||||
PAY_CHANNEL_ALIPAY_MINI: "支付宝小程序",
|
||||
PAY_CHANNEL_ALIPAY_JSAPI: "支付宝JSAPI",
|
||||
PAY_CHANNEL_PSBC: "邮储JSAPI",
|
||||
}
|
||||
|
||||
var OrderTypeList = map[int]string{
|
||||
|
|
@ -102,6 +106,6 @@ const (
|
|||
// 客户端环境和支付渠道的映射关系
|
||||
var OpenInPayChannelMap = map[int][]int{
|
||||
OpenInWeChat: {PAY_CHANNEL_WECHAT_JSAPI},
|
||||
OpenInMobile: {PAY_CHANNEL_WECHAT_H5, PAY_CHANNEL_ALIPAY_WEB},
|
||||
OpenInWindows: {PAY_CHANNEL_WECHAT_NATIVE, PAY_CHANNEL_ALIPAY_PC},
|
||||
OpenInMobile: {PAY_CHANNEL_WECHAT_H5, PAY_CHANNEL_ALIPAY_WEB, PAY_CHANNEL_PSBC},
|
||||
OpenInWindows: {PAY_CHANNEL_WECHAT_NATIVE, PAY_CHANNEL_ALIPAY_PC, PAY_CHANNEL_PSBC},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
package front
|
||||
|
||||
import (
|
||||
"PaymentCenter/app/constants/common"
|
||||
"PaymentCenter/app/constants/errorcode"
|
||||
"PaymentCenter/app/models/paychannelmodel"
|
||||
"PaymentCenter/app/services"
|
||||
"PaymentCenter/app/services/thirdpay/thirdpay_notify"
|
||||
"PaymentCenter/app/third/paymentService"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/qit-team/snow-core/log/logger"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PsbcCallback 邮储支付回调
|
||||
func PsbcCallback(c *gin.Context) {
|
||||
logger.Info(c, "PsbcCallback-回调数据", c.Request)
|
||||
payChannelId := c.Param("payChannelId")
|
||||
logger.Info(c, "PsbcCallback-回调数据payChannelId", payChannelId)
|
||||
if payChannelId == "" {
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
logger.Error(c, "PsbcCallback-读取请求体失败", err.Error())
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
bodyStr := string(body)
|
||||
logger.Info(c, "PsbcCallback-回调数据body", bodyStr)
|
||||
|
||||
var payChannelModel paychannelmodel.PayChannel
|
||||
payChannelIdInt, _ := strconv.Atoi(payChannelId)
|
||||
payChannelModel.Id = int64(payChannelIdInt)
|
||||
code := services.PayChannelGet(&payChannelModel)
|
||||
if code == errorcode.PayChannelNotFound {
|
||||
logger.Error(c, "PsbcCallback-回调数据未获取到支付配置,404")
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
|
||||
var psbcConfig paymentService.PsbcPay
|
||||
err = json.Unmarshal([]byte(payChannelModel.ExtJson), &psbcConfig)
|
||||
if err != nil {
|
||||
logger.Error(c, "PsbcCallback-回调数据解析支付配置错误", fmt.Sprintf("错误原因:%s", err.Error()))
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
if psbcConfig.MchtNo == "" || psbcConfig.PrivateKey == "" || psbcConfig.BankKey == "" {
|
||||
logger.Error(c, "PsbcCallback-回调数据解析支付配置错误,解析出来的信息为空")
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
psbcConfig.AppID = payChannelModel.AppId
|
||||
|
||||
orderId, amountTotal, payerTotal, status, err := paymentService.PsbcVerifyAndParseNotify(bodyStr, psbcConfig)
|
||||
if err != nil {
|
||||
logger.Error(c, "PsbcCallback-回调数据验签失败", err.Error())
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(c, "PsbcCallback-解析邮储回调结果", fmt.Sprintf("orderId:%d, amountTotal:%d, payerTotal:%d, status:%s", orderId, amountTotal, payerTotal, status))
|
||||
|
||||
var orderStatus int
|
||||
var errCode int
|
||||
var msg string
|
||||
switch status {
|
||||
case "SUCCESS":
|
||||
orderStatus = common.ORDER_STATUS_PAYED
|
||||
errCode = errorcode.Success
|
||||
msg = "支付成功"
|
||||
case "FAIL":
|
||||
orderStatus = common.ORDER_STATUS_FAILED
|
||||
errCode = errorcode.ParamError
|
||||
msg = "支付失败"
|
||||
default:
|
||||
logger.Error(c, "PsbcCallback-订单状态异常", status)
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
|
||||
notifyResult := thirdpay_notify.NewOrderNotifyWithHandle(orderId, errCode, orderStatus, int(amountTotal), int(payerTotal), msg)
|
||||
notifyResultJson, _ := json.Marshal(notifyResult)
|
||||
|
||||
go func() {
|
||||
payCallback := bodyStr
|
||||
payParam := "{}"
|
||||
merchantCallback := string(notifyResultJson)
|
||||
logStatus := common.THIRD_ORDER_LOG_STATUS_SUCCESS
|
||||
if notifyResult.ErrCode != errorcode.Success {
|
||||
logStatus = common.THIRD_ORDER_LOG_STATUS_FAIL
|
||||
}
|
||||
paymentService.SaveLog(orderId, common.THIRD_ORDER_TYPE_CALL_BACK, payCallback, payParam, merchantCallback, logStatus)
|
||||
}()
|
||||
|
||||
if notifyResult.ErrCode != errorcode.Success {
|
||||
logger.Error(c, "PsbcCallback-回调执行失败,失败原因:", fmt.Sprintf("%+v", notifyResult))
|
||||
c.String(http.StatusBadRequest, "%s", "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "%s", "success")
|
||||
return
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ func RegisterRoute(router *gin.Engine) {
|
|||
{
|
||||
notify.POST("/wx/:payChannelId", front.WxCallback)
|
||||
notify.POST("/ali/:payChannelId", front.AliCallback)
|
||||
notify.POST("/psbc/:payChannelId", front.PsbcCallback)
|
||||
}
|
||||
|
||||
pay := v1.Group("/pay", middlewares.ValidatePayRequest())
|
||||
|
|
|
|||
|
|
@ -179,6 +179,9 @@ func (w *Pay) PayUrl() (url string) {
|
|||
// 支付宝jsapi支付
|
||||
res = paymentService.PaymentService(*w.ctx, *thirdPay)
|
||||
|
||||
case common.PAY_CHANNEL_PSBC:
|
||||
res = paymentService.PaymentService(*w.ctx, *thirdPay)
|
||||
|
||||
default:
|
||||
w.PayCode = errorcode.PayChannelNotBuild
|
||||
return
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ var PayWayList = map[int]func(commonPayInfo *paymentService.PayOrderRequest, cha
|
|||
common.PAY_CHANNEL_WECHAT_JSAPI: WechatJSAPI,
|
||||
common.PAY_CHANNEL_WECHAT_MINI: WechatJSAPI,
|
||||
common.PAY_CHANNEL_ALIPAY_JSAPI: AlipayWeb,
|
||||
common.PAY_CHANNEL_PSBC: Psbc,
|
||||
}
|
||||
|
||||
func WechatH5(commonPayInfo *paymentService.PayOrderRequest, channel *paychannelmodel.PayChannel) error {
|
||||
|
|
@ -42,3 +43,12 @@ func WechatJSAPI(commonPayInfo *paymentService.PayOrderRequest, channel *paychan
|
|||
commonPayInfo.Wx.AppId = channel.AppId
|
||||
return nil
|
||||
}
|
||||
|
||||
func Psbc(commonPayInfo *paymentService.PayOrderRequest, channel *paychannelmodel.PayChannel) error {
|
||||
err := sonic.Unmarshal([]byte(channel.ExtJson), &commonPayInfo.Psbc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commonPayInfo.Psbc.AppID = channel.AppId
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ var RefundWayList = map[int]func(commonRefundInfo *paymentService.OrderRefundReq
|
|||
common.PAY_CHANNEL_ALIPAY_PC: AlipayWebRefund,
|
||||
common.PAY_CHANNEL_WECHAT_JSAPI: WechatJSAPIRefund,
|
||||
common.PAY_CHANNEL_WECHAT_MINI: WechatJSAPIRefund,
|
||||
common.PAY_CHANNEL_PSBC: PsbcRefund,
|
||||
}
|
||||
|
||||
func WechatH5Refund(commonRefundInfo *paymentService.OrderRefundRequest, channel *paychannelmodel.PayChannel) error {
|
||||
|
|
@ -44,3 +45,13 @@ func WechatJSAPIRefund(commonRefundInfo *paymentService.OrderRefundRequest, chan
|
|||
commonRefundInfo.Wx.AppId = channel.AppId
|
||||
return nil
|
||||
}
|
||||
|
||||
func PsbcRefund(commonRefundInfo *paymentService.OrderRefundRequest, channel *paychannelmodel.PayChannel) error {
|
||||
commonRefundInfo.PayChannel = common.PAY_CHANNLE_TYPE_PSBC
|
||||
err := sonic.Unmarshal([]byte(channel.ExtJson), &commonRefundInfo.Psbc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commonRefundInfo.Psbc.AppID = channel.AppId
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ func OrderQueryAndNotify(ctx context.Context, orderInfo *ordersmodel.OrdersLeftP
|
|||
AlipayRootCert: ali.AlipayRootCert,
|
||||
AlipayPublicCert: ali.AlipayPublicCert,
|
||||
}
|
||||
case common.PAY_CHANNLE_TYPE_PSBC:
|
||||
psbc := paymentService.PsbcPay{}
|
||||
_ = json.Unmarshal([]byte(orderInfo.ExtJson), &psbc)
|
||||
query.PayChannel = payCommon.PAY_CHANNLE_TYPE_PSBC
|
||||
psbc.AppID = orderInfo.AppId
|
||||
query.Psbc = psbc
|
||||
default:
|
||||
utils.Log(nil, "查询订单,支付渠道不支持", orderInfo.ChannelType)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const (
|
|||
PAY_CHANNEL_ALIPAY_JSAPI = 8
|
||||
PAY_CHANNEL_ALIPAY_PC = 9
|
||||
PAY_CHANNEL_ALIPAY_QrCode = 10
|
||||
PAY_CHANNEL_PSBC = 11
|
||||
|
||||
PAY_SUCCESS_CODE = 200 // 支付响应成功
|
||||
PAY_ERROR_CODE = 500 // 支付响应失败
|
||||
|
|
@ -27,11 +28,14 @@ const (
|
|||
WX_NOTIFY_URL_PROD = common.FRONT_V1 + "/notify/wx/" // 微信支付回调地址
|
||||
ALI_NOTIFY_URL_TEST = common.FRONT_V1 + "/notify/ali/" // 支付宝支付回调地址
|
||||
ALI_NOTIFY_URL_PROD = common.FRONT_V1 + "/notify/ali/" // 支付宝支付回调地址
|
||||
PSBC_NOTIFY_URL_TEST = common.FRONT_V1 + "/notify/psbc/" // 邮储支付回调地址测试环境
|
||||
PSBC_NOTIFY_URL_PROD = common.FRONT_V1 + "/notify/psbc/" // 邮储支付回调地址生产环境
|
||||
|
||||
ORDER_NO_TYPE_ORDER_NO = 2
|
||||
|
||||
PAY_CHANNLE_TYPE_WECHAT = 1 // 支付类型: 微信
|
||||
PAY_CHANNLE_TYPE_ZFB = 2 // 支付类型:支付宝
|
||||
PAY_CHANNLE_TYPE_PSBC = 3 // 支付类型:邮储
|
||||
|
||||
PAY_REFUND_STATU_COMMON = 0 // 未申请
|
||||
PAY_REFUND_STATU_ING = 1 // 退款中
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type PayOrderRequest struct {
|
|||
ReturnUrl string `json:"return_url"` // 支付成功后回调地址
|
||||
Wx WxPay `json:"wx"` // 微信支付参数
|
||||
Ali AliPay `json:"ali"` // 支付宝参数
|
||||
Psbc PsbcPay `json:"psbc"` // 邮储支付参数
|
||||
OpenId string `json:"open_id"` // 支付者信息。 openid
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +73,9 @@ func PaymentService(c context.Context, payOrderRequest PayOrderRequest) PayOrder
|
|||
case payCommon.PAY_CHANNEL_ALIPAY_JSAPI:
|
||||
// 支付宝JSAPI支付
|
||||
info, err = ALiJsApiPayInfo(c, payOrderRequest)
|
||||
case payCommon.PAY_CHANNEL_PSBC:
|
||||
// 邮储支付
|
||||
info, err = PsbcPayInfo(c, payOrderRequest)
|
||||
|
||||
default:
|
||||
payOrderResponse = PayOrderResponse{
|
||||
|
|
@ -113,6 +117,7 @@ type PayOrderQueryRequest struct {
|
|||
PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb
|
||||
Wx WxPay `json:"wx"`
|
||||
Ali AliPay `json:"ali"`
|
||||
Psbc PsbcPay `json:"psbc"`
|
||||
}
|
||||
|
||||
type PayOrderQueryResponse struct {
|
||||
|
|
@ -143,6 +148,8 @@ func PayOrderQuery(c context.Context, payOrderQueryRequest PayOrderQueryRequest)
|
|||
info, err = WxOrderQuery(c, payOrderQueryRequest.Wx, strconv.FormatInt(payOrderQueryRequest.OrderId, 10))
|
||||
case payCommon.PAY_CHANNLE_TYPE_ZFB:
|
||||
info, err = ALiOrderQuery(c, payOrderQueryRequest.Ali, strconv.FormatInt(payOrderQueryRequest.OrderId, 10))
|
||||
case payCommon.PAY_CHANNLE_TYPE_PSBC:
|
||||
info, err = PsbcOrderQuery(c, payOrderQueryRequest.Psbc, strconv.FormatInt(payOrderQueryRequest.OrderId, 10))
|
||||
default:
|
||||
payOrderQueryResponse = PayOrderQueryResponse{
|
||||
Code: payCommon.PAY_ERROR_CODE,
|
||||
|
|
@ -182,8 +189,10 @@ type OrderRefundRequest struct {
|
|||
RefundReason string `json:"refund_reason"` // 退款原因
|
||||
RefundAmount int64 `json:"refund_amount"` // 退款金额,单位分, 微信需要传入订单总额,不是用户实际支付金额
|
||||
PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb
|
||||
TransactionId string `json:"transaction_id"` // 第三方交易号
|
||||
Wx WxPay `json:"wx"`
|
||||
Ali AliPay `json:"ali"`
|
||||
Psbc PsbcPay `json:"psbc"`
|
||||
}
|
||||
|
||||
type OrderRefundResponse struct {
|
||||
|
|
@ -213,6 +222,8 @@ func OrderRefund(c context.Context, orderRefundRequest OrderRefundRequest) Order
|
|||
info, err = WxOrderRefund(c, orderRefundRequest)
|
||||
case payCommon.PAY_CHANNLE_TYPE_ZFB:
|
||||
info, err = AliRefundOrder(c, orderRefundRequest)
|
||||
case payCommon.PAY_CHANNLE_TYPE_PSBC:
|
||||
info, err = PsbcRefundOrder(c, orderRefundRequest)
|
||||
default:
|
||||
orderRefundResponse = OrderRefundResponse{
|
||||
Code: payCommon.PAY_ERROR_CODE,
|
||||
|
|
@ -252,6 +263,7 @@ type OrderRefundQueryRequest struct {
|
|||
PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb
|
||||
Wx WxPay `json:"wx"`
|
||||
Ali AliPay `json:"ali"`
|
||||
Psbc PsbcPay `json:"psbc"`
|
||||
}
|
||||
type OrderRefundQueryResponse struct {
|
||||
Code int `json:"code"`
|
||||
|
|
@ -308,6 +320,7 @@ type OrderCloseRequest struct {
|
|||
PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb
|
||||
Wx WxPay `json:"wx"`
|
||||
Ali AliPay `json:"ali"`
|
||||
Psbc PsbcPay `json:"psbc"`
|
||||
}
|
||||
|
||||
type OrderCloseResponse struct {
|
||||
|
|
@ -332,6 +345,8 @@ func OrderClose(c context.Context, orderCloseRequest OrderCloseRequest) OrderClo
|
|||
info, err = WxCloseOrder(c, orderCloseRequest)
|
||||
case payCommon.PAY_CHANNLE_TYPE_ZFB:
|
||||
info, err = AliCloseOrder(c, orderCloseRequest)
|
||||
case payCommon.PAY_CHANNLE_TYPE_PSBC:
|
||||
info, err = PsbcCloseOrder(c, orderCloseRequest)
|
||||
default:
|
||||
orderCloseResponse = OrderCloseResponse{
|
||||
Code: payCommon.PAY_ERROR_CODE,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
# Payment SDK
|
||||
|
||||
支付能力独立 SDK,封装邮储手机银行支付、查询、退款、对账单、回调验签等能力,供支付中心或其他业务系统复用。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 支付链接生成(SM2+SM4 加密)
|
||||
- 订单支付状态查询
|
||||
- 退款申请
|
||||
- 对账单查询与下载
|
||||
- 回调通知验签与解密
|
||||
- 工具方法:订单号生成、Map 转签名字符串、HMAC-SHA256 签名
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 初始化客户端
|
||||
|
||||
```go
|
||||
import "qteam/app/sdk/payment"
|
||||
|
||||
cfg := payment.Config{
|
||||
MerchantId: "yourMerchantId",
|
||||
MchtNo: "yourMchtNo",
|
||||
AppID: "yourAppId",
|
||||
SopPublicKey: "yourSopPublicKey",
|
||||
PrivateKey: "yourPrivateKey",
|
||||
Pubkey: "yourPubkey",
|
||||
BankKey: "yourBankKey",
|
||||
Sha: "yourShaKey",
|
||||
ReturnUrl: "https://your-domain.com/return",
|
||||
SuccessUrl: "https://your-domain.com/success",
|
||||
NotifyUrl: "https://your-domain.com/notify",
|
||||
ShowTitleBar: "1",
|
||||
LoginHost: "https://login.example.com/",
|
||||
OrderHost: "https://order.example.com/",
|
||||
FileHost: "https://file.example.com/",
|
||||
ShopId: "yourShopId",
|
||||
}
|
||||
|
||||
client := payment.NewClient(cfg)
|
||||
```
|
||||
|
||||
### 2. 创建支付链接
|
||||
|
||||
```go
|
||||
link, err := client.CreatePaymentLink(payment.PaymentLinkRequest{
|
||||
OrderNo: payment.GenerateOrderNumber(),
|
||||
ProductName: "测试商品",
|
||||
Price: "19.90",
|
||||
BackUrl: "https://mall.example.com/pay",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("支付链接:", link.PayUrl)
|
||||
fmt.Println("订单号:", link.OrderNo)
|
||||
fmt.Println("前端验签:", link.Sign)
|
||||
```
|
||||
|
||||
### 3. 查询订单
|
||||
|
||||
```go
|
||||
result, err := client.OrderQuery("SJ20240101000000001")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("订单状态:", result.OrderSta)
|
||||
fmt.Println("响应码:", result.RespCode)
|
||||
```
|
||||
|
||||
### 4. 申请退款
|
||||
|
||||
```go
|
||||
refund, err := client.Refund(payment.RefundRequest{
|
||||
OrderNo: "SJ20240101000000001",
|
||||
Price: "19.90",
|
||||
OrgTxnSeq: "bankOrderNo123",
|
||||
RefundDesc: "用户主动退款",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("退款订单号:", refund.RefundOrderNo)
|
||||
fmt.Println("退款状态:", refund.RefundOrderSta)
|
||||
```
|
||||
|
||||
### 5. 对账单查询与下载
|
||||
|
||||
```go
|
||||
// 查询对账单
|
||||
bill, err := client.BillQuery(time.Now())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 下载对账单
|
||||
for _, file := range bill.Files {
|
||||
fileId := file["fileId"].(string)
|
||||
data, err := client.BillDownload(fileId, time.Now())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// 处理对账单数据
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 处理回调通知
|
||||
|
||||
```go
|
||||
// 解密通知
|
||||
decrypted, err := client.DecryptNotify(rawJson)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 验签并解析
|
||||
notifyData, err := client.VerifyAndParseNotify(rawJson)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("通知数据:", notifyData)
|
||||
```
|
||||
|
||||
## 核心类型说明
|
||||
|
||||
### Config
|
||||
|
||||
支付配置,包含商户信息、密钥、URL 等必要配置。
|
||||
|
||||
### PaymentLinkRequest
|
||||
|
||||
```go
|
||||
type PaymentLinkRequest struct {
|
||||
OrderNo string
|
||||
ProductName string
|
||||
Price string
|
||||
BackUrl string
|
||||
}
|
||||
```
|
||||
|
||||
### PaymentLinkResponse
|
||||
|
||||
```go
|
||||
type PaymentLinkResponse struct {
|
||||
OrderNo string
|
||||
NotifyUrl string
|
||||
Sign string
|
||||
PlainText string
|
||||
PayUrl string
|
||||
}
|
||||
```
|
||||
|
||||
### OrderQueryResponse
|
||||
|
||||
```go
|
||||
type OrderQueryResponse struct {
|
||||
OrderNo string
|
||||
OrderSta string
|
||||
RespCode string
|
||||
RespMsg string
|
||||
}
|
||||
```
|
||||
|
||||
### RefundResponse
|
||||
|
||||
```go
|
||||
type RefundResponse struct {
|
||||
RespCode string
|
||||
RespMsg string
|
||||
RefundOrderNo string
|
||||
RefundOrderSta string
|
||||
TxnAmt string
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **密钥安全**:私钥、公钥等敏感信息请通过配置中心或环境变量传入,不要硬编码在代码中。
|
||||
2. **并发安全**:`GenerateOrderNumber` 和 `RandomNumber` 基于时间戳生成,高并发场景下建议由调用方保证唯一性。
|
||||
3. **错误处理**:所有接口均返回 `error`,调用方需根据业务场景处理重试、降级等逻辑。
|
||||
4. **状态码映射**:邮储订单状态码 `03-支付成功`、`04-支付失败`、`05-检查失败` 等,请参考 `types.go` 中的常量定义。
|
||||
|
||||
## 与现有系统集成建议
|
||||
|
||||
- **支付中心**:可直接使用此 SDK 封装统一支付服务。
|
||||
- **业务系统**:通过依赖此 SDK 接入支付能力,避免重复实现加密、验签逻辑。
|
||||
- **测试环境**:可通过 `Config` 切换测试环境地址和密钥。
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BillQuery 查询对账单
|
||||
func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
if date.IsZero() {
|
||||
date = time.Now().AddDate(0, 0, -1)
|
||||
}
|
||||
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
"reqTime": now,
|
||||
"method": "ufile.query.commonQuery",
|
||||
"version": "1",
|
||||
"merchantId": c.cfg.MerchantId,
|
||||
"appID": c.cfg.AppID,
|
||||
"accessType": "API",
|
||||
"reserve": "",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"date": date.Format("20060102"),
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
},
|
||||
}
|
||||
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
}
|
||||
|
||||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
responseData, err := c.DecryptResponse(string(body), false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密响应失败: %v", err)
|
||||
}
|
||||
|
||||
var result BillQueryResponse
|
||||
if err := json.Unmarshal([]byte(responseData), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// BillDownload 下载对账单
|
||||
func (c *Client) BillDownload(fileId string, date time.Time) ([]byte, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
"reqTime": now,
|
||||
"method": "ufile.download.commonDownload",
|
||||
"version": "1",
|
||||
"merchantId": c.cfg.MerchantId,
|
||||
"appID": c.cfg.AppID,
|
||||
"accessType": "API",
|
||||
"reserve": "",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"sm4Key": "",
|
||||
"fileId": fileId,
|
||||
},
|
||||
}
|
||||
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
}
|
||||
|
||||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ZZMarquis/gm/sm4"
|
||||
"github.com/tjfoc/gmsm/sm2"
|
||||
"github.com/tjfoc/gmsm/x509"
|
||||
)
|
||||
|
||||
// Client 支付客户端
|
||||
type Client struct {
|
||||
cfg Config
|
||||
}
|
||||
|
||||
// NewClient 创建支付客户端
|
||||
func NewClient(cfg Config) *Client {
|
||||
return &Client{cfg: cfg}
|
||||
}
|
||||
|
||||
// MapToString 将 map 转换为排序后的 key=value&key=value 格式
|
||||
func MapToString(m map[string]string) string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var result []string
|
||||
for _, k := range keys {
|
||||
result = append(result, fmt.Sprintf("%s=%s", k, m[k]))
|
||||
}
|
||||
return strings.Join(result, "&")
|
||||
}
|
||||
|
||||
// HmacSha256To16 生成 HMAC-SHA256 签名(16进制)
|
||||
func HmacSha256To16(key string, data string) string {
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(data))
|
||||
encode := mac.Sum(nil)
|
||||
return hex.EncodeToString(encode)
|
||||
}
|
||||
|
||||
// GenerateOrderNumber 生成订单号
|
||||
func GenerateOrderNumber() string {
|
||||
datePart := time.Now().Format("20060102150405")
|
||||
randomPart := fmt.Sprintf("%06d", time.Now().Nanosecond()%1000000)
|
||||
return "SJ" + datePart + randomPart
|
||||
}
|
||||
|
||||
// RandomNumber 生成指定长度的随机数字字符串
|
||||
func RandomNumber(n int) string {
|
||||
result := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = byte(time.Now().Nanosecond()%10 + '0')
|
||||
time.Sleep(1)
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// EncryptMobile 手机银行支付参数加密
|
||||
// 返回: tysdPayParams(base64), tysdEncryptKey(base64)
|
||||
func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("加密失败", err)
|
||||
}
|
||||
}()
|
||||
|
||||
sm4Key := generateSM4Key()
|
||||
|
||||
// SM2 加密 SM4 密钥(使用银行公钥)
|
||||
bankPubKey, err := x509.ReadPublicKeyFromHex(c.cfg.BankKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("读取银行公钥失败: %v", err)
|
||||
}
|
||||
encryptKeyBytes, err := sm2.Encrypt(bankPubKey, sm4Key, nil, sm2.C1C3C2)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("sm2加密失败: %v", err)
|
||||
}
|
||||
encryptKey := hex.EncodeToString(encryptKeyBytes)
|
||||
if len(encryptKey) > 2 && encryptKey[0:2] == "04" {
|
||||
encryptKeyBytes = encryptKeyBytes[1:]
|
||||
}
|
||||
|
||||
// SM2 签名
|
||||
merchantPrivKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("读取商户私钥失败: %v", err)
|
||||
}
|
||||
r, s, err := sm2.Sm2Sign(merchantPrivKey, []byte(signStr), []byte(c.cfg.MerchantId), rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("签名失败: %v", err)
|
||||
}
|
||||
sig := encodeBase64(rsToBytes(r, s))
|
||||
|
||||
// 拼接参数并 SM4 ECB 加密
|
||||
param := fmt.Sprintf("%s&sign=%s", inputJson, sig)
|
||||
tmp, err := sm4ECBEncrypt(sm4Key, pkcs5Padding([]byte(param), 16))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("sm4加密失败: %v", err)
|
||||
}
|
||||
|
||||
return encodeBase64(tmp), encodeBase64(encryptKeyBytes), nil
|
||||
}
|
||||
|
||||
// DecryptResponse 解密银行响应
|
||||
func (c *Client) DecryptResponse(respJson string, isRequest bool) (string, error) {
|
||||
var reqData map[string]string
|
||||
if err := json.Unmarshal([]byte(respJson), &reqData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqData["accessToken"] = ""
|
||||
var keys []string
|
||||
if isRequest {
|
||||
keys = []string{"request", "signature", "encryptKey", "accessToken"}
|
||||
} else {
|
||||
keys = []string{"response", "signature", "encryptKey", "accessToken"}
|
||||
}
|
||||
|
||||
var inEncryptKey, inData, inSignature string
|
||||
for _, key := range keys {
|
||||
data, ok := reqData[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("请求数据中不存在%s", key)
|
||||
}
|
||||
switch key {
|
||||
case "request", "response":
|
||||
inData = data
|
||||
case "signature":
|
||||
inSignature = data
|
||||
case "encryptKey":
|
||||
inEncryptKey = data
|
||||
}
|
||||
}
|
||||
|
||||
// 验签
|
||||
checked := c.verify(fmt.Sprintf("%s%s%s", inData, inEncryptKey, ""), inSignature)
|
||||
if !checked {
|
||||
return "", fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
// 解密 SM4 密钥
|
||||
privKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取私钥失败: %v", err)
|
||||
}
|
||||
hexEncryptKey, err := hex.DecodeString(inEncryptKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解密sm4key失败: %v", err)
|
||||
}
|
||||
sm4Key, err := sm2.Decrypt(privKey, hexEncryptKey, sm2.C1C3C2)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解密sm2key失败: %v", err)
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
request, _ := base64.StdEncoding.DecodeString(inData)
|
||||
encryptedSm4Key, err := sm4.CBCDecrypt(sm4Key, getSM4IV(), request)
|
||||
return string(pkcs5UnPadding(encryptedSm4Key)), nil
|
||||
}
|
||||
|
||||
// VerifySignature 验签
|
||||
func (c *Client) VerifySignature(content, signature string) bool {
|
||||
return c.verify(content, signature)
|
||||
}
|
||||
|
||||
func (c *Client) verify(content, signature string) bool {
|
||||
pubKey, err := x509.ReadPublicKeyFromHex(c.cfg.SopPublicKey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("pubKeyBytes sm2 ReadPublicKeyFromHex err: %v", err))
|
||||
}
|
||||
r, s := signToRS(signature)
|
||||
return sm2.Sm2Verify(pubKey, []byte(content), []byte(c.cfg.MerchantId), r, s)
|
||||
}
|
||||
|
||||
func signToRS(signStr string) (*big.Int, *big.Int) {
|
||||
sub := strings.Split(signStr, "#")
|
||||
if len(sub) != 2 {
|
||||
panic(fmt.Sprintf("err rs: %x", sub))
|
||||
}
|
||||
r, _ := new(big.Int).SetString(sub[0], 16)
|
||||
s, _ := new(big.Int).SetString(sub[1], 16)
|
||||
return r, s
|
||||
}
|
||||
|
||||
func rsToBytes(r, s *big.Int) []byte {
|
||||
rBytes := r.Bytes()
|
||||
sBytes := s.Bytes()
|
||||
result := make([]byte, 0, len(rBytes)+len(sBytes))
|
||||
result = append(result, rBytes...)
|
||||
result = append(result, sBytes...)
|
||||
return result
|
||||
}
|
||||
|
||||
func encodeBase64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func getSM4IV() []byte {
|
||||
return []byte("UISwD9fW6cFh9SNS")
|
||||
}
|
||||
|
||||
func generateSM4Key() []byte {
|
||||
return make([]byte, 16)
|
||||
}
|
||||
|
||||
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := make([]byte, padding)
|
||||
for i := 0; i < padding; i++ {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func pkcs5UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
if length == 0 {
|
||||
return origData
|
||||
}
|
||||
unpadding := int(origData[length-1])
|
||||
if unpadding > length {
|
||||
return origData
|
||||
}
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
func sm4ECBEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
block, err := sm4.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
plaintext = pkcs5Padding(plaintext, blockSize)
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
for start := 0; start < len(plaintext); start += blockSize {
|
||||
block.Encrypt(ciphertext[start:start+blockSize], plaintext[start:start+blockSize])
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EncryptRequest 加密支付请求
|
||||
func EncryptRequest(request interface{}, cfg Config) (map[string]string, error) {
|
||||
_, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求失败: %v", err)
|
||||
}
|
||||
|
||||
client := NewClient(cfg)
|
||||
|
||||
// 构造签名数据
|
||||
signData := map[string]string{
|
||||
"merchantNo": cfg.MchtNo,
|
||||
}
|
||||
signStr := MapToString(signData)
|
||||
|
||||
// 构造订单数据
|
||||
orderData := map[string]string{
|
||||
"merchantNo": cfg.MchtNo,
|
||||
"orderNo": "",
|
||||
"amount": "",
|
||||
}
|
||||
orderStr := MapToString(orderData)
|
||||
|
||||
// 加密
|
||||
encryptedData, encryptedKey, err := client.EncryptMobile(orderStr, signStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"request": encryptedData,
|
||||
"signature": "",
|
||||
"encryptKey": encryptedKey,
|
||||
"accessToken": "",
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DecryptNotify 解密回调通知
|
||||
func (c *Client) DecryptNotify(rawJson string) (string, error) {
|
||||
return c.DecryptResponse(rawJson, true)
|
||||
}
|
||||
|
||||
// VerifyNotifySignature 验证回调通知签名
|
||||
func (c *Client) VerifyNotifySignature(content, signature string) bool {
|
||||
return c.VerifySignature(content, signature)
|
||||
}
|
||||
|
||||
// ParseNotify 解析回调通知
|
||||
func (c *Client) ParseNotify(rawJson string) (*NotifyRequest, error) {
|
||||
var req NotifyRequest
|
||||
if err := json.Unmarshal([]byte(rawJson), &req); err != nil {
|
||||
return nil, fmt.Errorf("解析通知失败: %v", err)
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
// VerifyAndParseNotify 验签并解析回调通知
|
||||
func (c *Client) VerifyAndParseNotify(rawJson string) (map[string]interface{}, error) {
|
||||
var reqData map[string]string
|
||||
if err := json.Unmarshal([]byte(rawJson), &reqData); err != nil {
|
||||
return nil, fmt.Errorf("解析通知失败: %v", err)
|
||||
}
|
||||
|
||||
signature := reqData["signature"]
|
||||
delete(reqData, "signature")
|
||||
|
||||
content := MapToString(reqData)
|
||||
if !c.VerifySignature(content, signature) {
|
||||
return nil, fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
decrypted, err := c.DecryptNotify(rawJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密通知失败: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(decrypted), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析解密后数据失败: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OrderQuery 查询订单支付状态
|
||||
func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
"reqTime": now,
|
||||
"method": "b2c.gatewaypay.orderQuery",
|
||||
"version": "1",
|
||||
"merchantId": c.cfg.MerchantId,
|
||||
"appID": c.cfg.AppID,
|
||||
"accessType": "API",
|
||||
"reserve": "",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"busiMainId": BusiMainId,
|
||||
"reqTransTime": now,
|
||||
"data": map[string]string{
|
||||
"txnCode": "1004",
|
||||
"sourceId": "16",
|
||||
"reqTraceId": now + RandomNumber(10),
|
||||
"reqDate": time.Now().Format("20060102"),
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"oldSeqNo": orderNo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
}
|
||||
|
||||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
responseData, err := c.DecryptResponse(string(body), false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密响应失败: %v", err)
|
||||
}
|
||||
|
||||
var result OrderQueryResponse
|
||||
if err := json.Unmarshal([]byte(responseData), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// CreatePaymentLink 创建支付链接
|
||||
func (c *Client) CreatePaymentLink(req PaymentLinkRequest) (*PaymentLinkResponse, error) {
|
||||
payUrlTemplate := "%s&fromId=include_sb&tysdPayParams=%s&tysdShopid=%s&tysdEncryptKey=%s&returnUrl=%s&showTitleBar=%s"
|
||||
|
||||
orderData := map[string]string{
|
||||
"merchantNo": c.cfg.MchtNo,
|
||||
"orderInfos": req.ProductName,
|
||||
"orderNo": req.OrderNo,
|
||||
"amount": req.Price,
|
||||
"successUrl": hex.EncodeToString([]byte(c.cfg.SuccessUrl + req.OrderNo)),
|
||||
"acctType": "2",
|
||||
"notifyUrl": hex.EncodeToString([]byte(c.cfg.SuccessUrl + req.OrderNo)),
|
||||
}
|
||||
|
||||
signData := map[string]string{
|
||||
"merchantNo": c.cfg.MchtNo,
|
||||
"orderNo": req.OrderNo,
|
||||
"amount": req.Price,
|
||||
}
|
||||
|
||||
sign, enKey, err := c.EncryptMobile(MapToString(orderData), MapToString(signData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密支付参数失败: %v", err)
|
||||
}
|
||||
|
||||
payUrl := fmt.Sprintf(payUrlTemplate,
|
||||
req.BackUrl,
|
||||
sign,
|
||||
c.cfg.ShopId,
|
||||
enKey,
|
||||
url.QueryEscape(c.cfg.ReturnUrl),
|
||||
c.cfg.ShowTitleBar,
|
||||
)
|
||||
|
||||
plainText := c.cfg.MchtNo + "|" + req.OrderNo + "|" + req.Price
|
||||
signature := HmacSha256To16(c.cfg.Sha, plainText)
|
||||
|
||||
return &PaymentLinkResponse{
|
||||
OrderNo: req.OrderNo,
|
||||
NotifyUrl: c.cfg.NotifyUrl,
|
||||
Sign: signature,
|
||||
PlainText: plainText,
|
||||
PayUrl: payUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreatePaymentLinkWithBackUrl 创建支付链接(带 fromId 判断)
|
||||
func (c *Client) CreatePaymentLinkWithBackUrl(req PaymentLinkRequest) (*PaymentLinkResponse, error) {
|
||||
var payUrlTemplate string
|
||||
if len(req.BackUrl) > 0 && (contains(req.BackUrl, "fromId")) {
|
||||
payUrlTemplate = "%s&tysdPayParams=%s&tysdShopid=%s&tysdEncryptKey=%s&returnUrl=%s&showTitleBar=%s"
|
||||
} else {
|
||||
payUrlTemplate = "%s&fromId=include_sb&tysdPayParams=%s&tysdShopid=%s&tysdEncryptKey=%s&returnUrl=%s&showTitleBar=%s"
|
||||
}
|
||||
|
||||
orderData := map[string]string{
|
||||
"merchantNo": c.cfg.MchtNo,
|
||||
"orderInfos": req.ProductName,
|
||||
"orderNo": req.OrderNo,
|
||||
"amount": req.Price,
|
||||
"successUrl": hex.EncodeToString([]byte(c.cfg.SuccessUrl + req.OrderNo)),
|
||||
"acctType": "2",
|
||||
"notifyUrl": hex.EncodeToString([]byte(c.cfg.SuccessUrl + req.OrderNo)),
|
||||
}
|
||||
|
||||
signData := map[string]string{
|
||||
"merchantNo": c.cfg.MchtNo,
|
||||
"orderNo": req.OrderNo,
|
||||
"amount": req.Price,
|
||||
}
|
||||
|
||||
sign, enKey, err := c.EncryptMobile(MapToString(orderData), MapToString(signData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密支付参数失败: %v", err)
|
||||
}
|
||||
|
||||
payUrl := fmt.Sprintf(payUrlTemplate,
|
||||
req.BackUrl,
|
||||
sign,
|
||||
c.cfg.ShopId,
|
||||
enKey,
|
||||
url.QueryEscape(c.cfg.ReturnUrl),
|
||||
c.cfg.ShowTitleBar,
|
||||
)
|
||||
|
||||
plainText := c.cfg.MchtNo + "|" + req.OrderNo + "|" + req.Price
|
||||
signature := HmacSha256To16(c.cfg.Sha, plainText)
|
||||
|
||||
return &PaymentLinkResponse{
|
||||
OrderNo: req.OrderNo,
|
||||
NotifyUrl: c.cfg.NotifyUrl,
|
||||
Sign: signature,
|
||||
PlainText: plainText,
|
||||
PayUrl: payUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) > len(substr) && s[:len(substr)] == substr)
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Refund 申请退款
|
||||
func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
"reqTime": now,
|
||||
"method": "b2c.gatewaypay.orderRefund",
|
||||
"version": "1",
|
||||
"merchantId": c.cfg.MerchantId,
|
||||
"appID": c.cfg.AppID,
|
||||
"accessType": "API",
|
||||
"reserve": "",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"busiMainId": BusiMainId,
|
||||
"reqTransTime": now,
|
||||
"data": map[string]string{
|
||||
"txnCode": "1003",
|
||||
"sourceId": "16",
|
||||
"reqTraceId": req.OrderNo + now[:8],
|
||||
"reqDate": now[:8],
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"orgTxnSeq": req.OrgTxnSeq,
|
||||
"txnAmt": req.Price,
|
||||
"orgTxnAmt": req.Price,
|
||||
"refundDesc": "用户主动退款",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
}
|
||||
|
||||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
responseData, err := c.DecryptResponse(string(body), false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密响应失败: %v", err)
|
||||
}
|
||||
|
||||
var result RefundResponse
|
||||
if err := json.Unmarshal([]byte(responseData), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config 支付配置
|
||||
type Config struct {
|
||||
MerchantId string
|
||||
MchtNo string
|
||||
AppID string
|
||||
SopPublicKey string
|
||||
PrivateKey string
|
||||
Pubkey string
|
||||
BankKey string
|
||||
Sha string
|
||||
ReturnUrl string
|
||||
SuccessUrl string
|
||||
NotifyUrl string
|
||||
ShowTitleBar string
|
||||
LoginHost string
|
||||
OrderHost string
|
||||
FileHost string
|
||||
ShopId string
|
||||
}
|
||||
|
||||
// PaymentLinkRequest 生成支付链接请求
|
||||
type PaymentLinkRequest struct {
|
||||
OrderNo string
|
||||
ProductName string
|
||||
Price string
|
||||
BackUrl string
|
||||
}
|
||||
|
||||
// PaymentLinkResponse 生成支付链接响应
|
||||
type PaymentLinkResponse struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
NotifyUrl string `json:"notify_url"`
|
||||
Sign string `json:"sign"`
|
||||
PlainText string `json:"plain_text"`
|
||||
PayUrl string `json:"pay_url"`
|
||||
}
|
||||
|
||||
// OrderQueryRequest 订单查询请求
|
||||
type OrderQueryRequest struct {
|
||||
OrderNo string
|
||||
}
|
||||
|
||||
// OrderQueryResponse 订单查询响应
|
||||
type OrderQueryResponse struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
OrderSta string `json:"orderSta"`
|
||||
RespCode string `json:"code"`
|
||||
RespMsg string `json:"respMsg"`
|
||||
}
|
||||
|
||||
// RefundRequest 退款请求
|
||||
type RefundRequest struct {
|
||||
OrderNo string
|
||||
Price string
|
||||
OrgTxnSeq string
|
||||
RefundDesc string
|
||||
}
|
||||
|
||||
// RefundResponse 退款响应
|
||||
type RefundResponse struct {
|
||||
RespCode string `json:"code"`
|
||||
RespMsg string `json:"respMsg"`
|
||||
RefundOrderNo string `json:"refundOrderNo"`
|
||||
RefundOrderSta string `json:"refundOrderSta"`
|
||||
TxnAmt string `json:"txnAmt"`
|
||||
}
|
||||
|
||||
// BillQueryRequest 对账单查询请求
|
||||
type BillQueryRequest struct {
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
// BillQueryResponse 对账单查询响应
|
||||
type BillQueryResponse struct {
|
||||
Files []map[string]interface{} `json:"files"`
|
||||
}
|
||||
|
||||
// BillDownloadRequest 对账单下载请求
|
||||
type BillDownloadRequest struct {
|
||||
FileId string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
// NotifyRequest 回调通知请求
|
||||
type NotifyRequest struct {
|
||||
Request string `json:"request"`
|
||||
Signature string `json:"signature"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
EncryptKey string `json:"encryptKey"`
|
||||
}
|
||||
|
||||
// NotifyResponse 回调通知响应
|
||||
type NotifyResponse struct {
|
||||
RespCode string `json:"respCode"`
|
||||
RespMsg string `json:"respMsg"`
|
||||
Data interface{} `json:"data"`
|
||||
LinkUrl string `json:"linkUrl"`
|
||||
}
|
||||
|
||||
// OrderStatus 订单状态
|
||||
type OrderStatus string
|
||||
|
||||
const (
|
||||
OrderStatusWait OrderStatus = "01"
|
||||
OrderStatusSuccess OrderStatus = "03"
|
||||
OrderStatusFail OrderStatus = "04"
|
||||
OrderStatusCheckFail OrderStatus = "05"
|
||||
)
|
||||
|
||||
// RefundStatus 退款状态
|
||||
type RefundStatus string
|
||||
|
||||
const (
|
||||
RefundStatusSuccess RefundStatus = "03"
|
||||
RefundStatusFail RefundStatus = "04"
|
||||
)
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package paymentService
|
||||
|
||||
import (
|
||||
"PaymentCenter/app/third/paymentService/payCommon"
|
||||
psbc "PaymentCenter/app/third/paymentService/psbc"
|
||||
"PaymentCenter/config"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/qit-team/snow-core/log/logger"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type PsbcPay struct {
|
||||
MerchantId string `json:"merchant_id"` // 商户ID
|
||||
MchtNo string `json:"mcht_no"` // 商户号
|
||||
AppID string `json:"app_id"` // 应用ID
|
||||
SopPublicKey string `json:"sop_public_key"` // SOP公钥
|
||||
PrivateKey string `json:"private_key"` // 商户私钥
|
||||
Pubkey string `json:"pubkey"` // 商户公钥
|
||||
BankKey string `json:"bank_key"` // 银行公钥
|
||||
Sha string `json:"sha"` // SHA密钥
|
||||
ReturnUrl string `json:"return_url"` // 支付返回地址
|
||||
SuccessUrl string `json:"success_url"` // 支付成功地址
|
||||
NotifyUrl string `json:"notify_url"` // 回调通知地址
|
||||
ShowTitleBar string `json:"show_title_bar"` // 是否显示标题栏
|
||||
LoginHost string `json:"login_host"` // 登录服务地址
|
||||
OrderHost string `json:"order_host"` // 订单服务地址
|
||||
FileHost string `json:"file_host"` // 文件服务地址
|
||||
ShopId string `json:"shop_id"` // 店铺ID
|
||||
}
|
||||
|
||||
func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client {
|
||||
cfg := psbc.Config{
|
||||
MerchantId: psbcConfig.MerchantId,
|
||||
MchtNo: psbcConfig.MchtNo,
|
||||
AppID: psbcConfig.AppID,
|
||||
SopPublicKey: psbcConfig.SopPublicKey,
|
||||
PrivateKey: psbcConfig.PrivateKey,
|
||||
Pubkey: psbcConfig.Pubkey,
|
||||
BankKey: psbcConfig.BankKey,
|
||||
Sha: psbcConfig.Sha,
|
||||
ReturnUrl: psbcConfig.ReturnUrl,
|
||||
SuccessUrl: psbcConfig.SuccessUrl,
|
||||
NotifyUrl: psbcConfig.NotifyUrl,
|
||||
ShowTitleBar: psbcConfig.ShowTitleBar,
|
||||
LoginHost: psbcConfig.LoginHost,
|
||||
OrderHost: psbcConfig.OrderHost,
|
||||
FileHost: psbcConfig.FileHost,
|
||||
ShopId: psbcConfig.ShopId,
|
||||
}
|
||||
return psbc.NewClient(cfg)
|
||||
}
|
||||
|
||||
func PsbcPayInfo(c context.Context, payOrderRequest PayOrderRequest) (string, error) {
|
||||
envConfig := config.GetConf()
|
||||
notifyUrl := fmt.Sprintf(envConfig.PayService.Host+payCommon.PSBC_NOTIFY_URL_TEST+"%d", payOrderRequest.PayChannelId)
|
||||
|
||||
psbcConfig := payOrderRequest.Psbc
|
||||
psbcConfig.NotifyUrl = notifyUrl
|
||||
psbcClient := PsbcInitClient(psbcConfig)
|
||||
|
||||
amount := fmt.Sprintf("%.2f", float64(payOrderRequest.Amount)/100.0)
|
||||
orderNo := strconv.FormatInt(payOrderRequest.OrderId, 10)
|
||||
|
||||
req := psbc.PaymentLinkRequest{
|
||||
OrderNo: orderNo,
|
||||
ProductName: payOrderRequest.Description,
|
||||
Price: amount,
|
||||
BackUrl: payOrderRequest.ReturnUrl,
|
||||
}
|
||||
|
||||
resp, err := psbcClient.CreatePaymentLink(req)
|
||||
if err != nil {
|
||||
logger.Error(c, "PsbcPayInfo 发生错误", fmt.Sprintf("错误信息:%s", err.Error()))
|
||||
return "", err
|
||||
}
|
||||
return resp.PayUrl, nil
|
||||
}
|
||||
|
||||
func PsbcOrderQuery(ctx context.Context, psbcConfig PsbcPay, orderNo string) (PayOrderQueryInfo, error) {
|
||||
psbcClient := PsbcInitClient(psbcConfig)
|
||||
|
||||
resp, err := psbcClient.OrderQuery(orderNo)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "PsbcOrderQuery 发生错误", fmt.Sprintf("错误信息:%s", err.Error()))
|
||||
return PayOrderQueryInfo{}, err
|
||||
}
|
||||
|
||||
tradeState := "NOTPAY"
|
||||
tradeStateDesc := ""
|
||||
switch resp.OrderSta {
|
||||
case "03":
|
||||
tradeState = "SUCCESS"
|
||||
tradeStateDesc = "支付成功"
|
||||
case "04", "05":
|
||||
tradeState = "FAIL"
|
||||
tradeStateDesc = "支付失败"
|
||||
default:
|
||||
tradeState = "NOTPAY"
|
||||
tradeStateDesc = "未支付"
|
||||
}
|
||||
|
||||
amountTotal := int64(0)
|
||||
payerTotal := int64(0)
|
||||
|
||||
outTradeNo, _ := strconv.ParseInt(orderNo, 10, 64)
|
||||
return PayOrderQueryInfo{
|
||||
AppId: psbcConfig.AppID,
|
||||
OutTradeNo: outTradeNo,
|
||||
TransactionId: "",
|
||||
TradeState: tradeState,
|
||||
TradeStateDesc: tradeStateDesc,
|
||||
SuccessTime: "",
|
||||
AmountTotal: amountTotal,
|
||||
PayerTotal: payerTotal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PsbcRefundOrder(ctx context.Context, orderRefundRequest OrderRefundRequest) (OrderRefundInfo, error) {
|
||||
psbcClient := PsbcInitClient(orderRefundRequest.Psbc)
|
||||
|
||||
refundAmount := fmt.Sprintf("%.2f", float64(orderRefundRequest.RefundAmount)/100.0)
|
||||
orderNo := strconv.FormatInt(orderRefundRequest.OrderId, 10)
|
||||
|
||||
req := psbc.RefundRequest{
|
||||
OrderNo: orderNo,
|
||||
Price: refundAmount,
|
||||
OrgTxnSeq: orderRefundRequest.TransactionId,
|
||||
RefundDesc: orderRefundRequest.RefundReason,
|
||||
}
|
||||
|
||||
resp, err := psbcClient.Refund(req)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "PsbcRefundOrder 发生错误", fmt.Sprintf("申请退款接口失败,错误信息:%s", err.Error()))
|
||||
return OrderRefundInfo{}, err
|
||||
}
|
||||
|
||||
refundStatus := payCommon.PAY_REFUND_STATU_COMMON
|
||||
switch resp.RefundOrderSta {
|
||||
case "03":
|
||||
refundStatus = payCommon.PAY_REFUND_STATU_SUCCESS
|
||||
case "04":
|
||||
refundStatus = payCommon.PAY_REFUND_STATU_FAIL
|
||||
}
|
||||
|
||||
refundFee := int64(0)
|
||||
if resp.TxnAmt != "" {
|
||||
amountFloat, err := strconv.ParseFloat(resp.TxnAmt, 64)
|
||||
if err == nil {
|
||||
refundFee = int64(amountFloat * 100)
|
||||
}
|
||||
}
|
||||
|
||||
return OrderRefundInfo{
|
||||
OutTradeNo: orderRefundRequest.OrderId,
|
||||
TransactionId: resp.RefundOrderNo,
|
||||
RefundFee: refundFee,
|
||||
RefundOrderId: orderRefundRequest.RefundOrderId,
|
||||
RefundStatus: refundStatus,
|
||||
RefundSuccessTime: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PsbcCloseOrder(ctx context.Context, orderCloseRequest OrderCloseRequest) (OrderCloseInfo, error) {
|
||||
return OrderCloseInfo{
|
||||
OutTradeNo: orderCloseRequest.OrderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PsbcVerifyAndParseNotify(rawBody string, psbcConfig PsbcPay) (orderId int64, amountTotal int64, payerTotal int64, status string, err error) {
|
||||
psbcClient := PsbcInitClient(psbcConfig)
|
||||
|
||||
result, err := psbcClient.VerifyAndParseNotify(rawBody)
|
||||
if err != nil {
|
||||
logger.Error(context.Background(), "PsbcVerifyAndParseNotify 发生错误", fmt.Sprintf("验签解密失败,错误信息:%s", err.Error()))
|
||||
return 0, 0, 0, "", err
|
||||
}
|
||||
|
||||
orderNoStr := ""
|
||||
if orderNo, ok := result["orderNo"].(string); ok {
|
||||
orderNoStr = orderNo
|
||||
}
|
||||
if orderNoStr == "" {
|
||||
return 0, 0, 0, "", errors.New("回调数据中缺少订单号")
|
||||
}
|
||||
|
||||
orderId, err = strconv.ParseInt(orderNoStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Error(context.Background(), "PsbcVerifyAndParseNotify 发生错误", fmt.Sprintf("订单号转换失败,错误信息:%s", err.Error()))
|
||||
return 0, 0, 0, "", err
|
||||
}
|
||||
|
||||
amountStr := ""
|
||||
if amount, ok := result["amount"].(string); ok {
|
||||
amountStr = amount
|
||||
}
|
||||
if amountStr != "" {
|
||||
amountFloat, err := strconv.ParseFloat(amountStr, 64)
|
||||
if err == nil {
|
||||
amountTotal = int64(amountFloat * 100)
|
||||
payerTotal = amountTotal
|
||||
}
|
||||
}
|
||||
|
||||
orderSta := ""
|
||||
if sta, ok := result["orderSta"].(string); ok {
|
||||
orderSta = sta
|
||||
}
|
||||
|
||||
switch orderSta {
|
||||
case "03":
|
||||
status = "SUCCESS"
|
||||
case "04", "05":
|
||||
status = "FAIL"
|
||||
default:
|
||||
status = "NOTPAY"
|
||||
}
|
||||
|
||||
return orderId, amountTotal, payerTotal, status, nil
|
||||
}
|
||||
|
|
@ -457,33 +457,13 @@ func PayType(payChannel int) int {
|
|||
return common.PAY_CHANNLE_TYPE_WECHAT
|
||||
case common.PAY_CHANNEL_ALIPAY_JSAPI, common.PAY_CHANNEL_ALIPAY_WEB, common.PAY_CHANNEL_ALIPAY_MINI, common.PAY_CHANNEL_ALIPAY_PC:
|
||||
return common.PAY_CHANNLE_TYPE_ZFB
|
||||
case common.PAY_CHANNEL_PSBC:
|
||||
return common.PAY_CHANNLE_TYPE_PSBC
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// redis控制访问频率
|
||||
func KeyExistsOrSet(key string, expire int) bool {
|
||||
redisClient := redis.GetRedis()
|
||||
|
||||
// 假设Exists方法返回的是int64类型,表示键是否存在(0为不存在,1为存在)
|
||||
if exists, err := redisClient.Exists(context.Background(), key).Result(); err == nil && exists == 1 {
|
||||
return true
|
||||
} else if err != nil {
|
||||
// 处理错误情况,可以根据实际情况记录日志或返回错误
|
||||
Log(nil, "redis_Exists异常", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果键不存在,则设置键并返回false
|
||||
if err := redisClient.SetEX(context.Background(), key, "", time.Duration(expire)*time.Second).Err(); err != nil {
|
||||
// 处理设置键时的错误情况
|
||||
Log(nil, "redis_SetEX异常", err)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AcquireLock 尝试获取分布式锁,localValue用于检测是否是同一个客户端持有的锁锁
|
||||
func AcquireLock(key, lockValue string, expired time.Duration) (bool, error) {
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
Loading…
Reference in New Issue