feat:邮储支付

This commit is contained in:
wolter 2026-07-28 09:46:02 +08:00
parent b91923c44f
commit a2202ea200
22 changed files with 1834 additions and 122 deletions

443
README.md
View File

@ -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 |
| 缓存/分布式锁 | Redissnow-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 路由
MiddlewareCORS → ValidateRequest / ValidatePayRequest → 加解密)
Controllerfront/ 或 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、封装返回
- **服务层**:核心业务(状态机、幂等、分布式锁、渠道路由)
- **Repositorydata/**:只做数据库 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) |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 // 退款中

View File

@ -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,

View File

@ -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` 切换测试环境地址和密钥。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
)

View File

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

View File

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

2
logs/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!.gitignore