diff --git a/README.md b/README.md index bc0068b..f88105c 100644 --- a/README.md +++ b/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. 架构分层与调用链 -## - \ No newline at end of file +### 7.3 加密算法(由 `app.key_type` 决定) + +| 类型 | 实现文件 | +|------|---------| +| 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) | diff --git a/app/constants/common/common.go b/app/constants/common/common.go index 04deb94..18a4fec 100644 --- a/app/constants/common/common.go +++ b/app/constants/common/common.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}, } diff --git a/app/http/controllers/front/psbc.go b/app/http/controllers/front/psbc.go new file mode 100644 index 0000000..07a0c4c --- /dev/null +++ b/app/http/controllers/front/psbc.go @@ -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 +} diff --git a/app/http/routes/route.go b/app/http/routes/route.go index 9e2b136..a22da57 100644 --- a/app/http/routes/route.go +++ b/app/http/routes/route.go @@ -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()) diff --git a/app/services/thirdpay/do/pay.go b/app/services/thirdpay/do/pay.go index 976cbdd..45e9463 100644 --- a/app/services/thirdpay/do/pay.go +++ b/app/services/thirdpay/do/pay.go @@ -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 diff --git a/app/services/thirdpay/do/pay_way.go b/app/services/thirdpay/do/pay_way.go index 4c6fe6b..56f302d 100644 --- a/app/services/thirdpay/do/pay_way.go +++ b/app/services/thirdpay/do/pay_way.go @@ -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 +} diff --git a/app/services/thirdpay/do/refund_way.go b/app/services/thirdpay/do/refund_way.go index 33ce693..0445b5e 100644 --- a/app/services/thirdpay/do/refund_way.go +++ b/app/services/thirdpay/do/refund_way.go @@ -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 +} diff --git a/app/services/thirdpay/pay_query.go b/app/services/thirdpay/pay_query.go index 634aca5..3ed3300 100644 --- a/app/services/thirdpay/pay_query.go +++ b/app/services/thirdpay/pay_query.go @@ -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 diff --git a/app/third/paymentService/payCommon/common.go b/app/third/paymentService/payCommon/common.go index 9baefd5..8b1c372 100644 --- a/app/third/paymentService/payCommon/common.go +++ b/app/third/paymentService/payCommon/common.go @@ -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 // 支付响应失败 @@ -23,15 +24,18 @@ const ( WX_SUCCESS_CODE = 200 // 微信状态码返回成功 - WX_NOTIFY_URL_TEST = common.FRONT_V1 + "/notify/wx/" // 微信支付回调地址 - 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/" // 支付宝支付回调地址 + WX_NOTIFY_URL_TEST = common.FRONT_V1 + "/notify/wx/" // 微信支付回调地址 + 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 // 退款中 diff --git a/app/third/paymentService/payment_service.go b/app/third/paymentService/payment_service.go index 7ae426c..e485092 100644 --- a/app/third/paymentService/payment_service.go +++ b/app/third/paymentService/payment_service.go @@ -13,16 +13,17 @@ import ( ) type PayOrderRequest struct { - PayChannelId int64 `json:"pay_channel_id"` // 支付方式ID - OrderId int64 `json:"order_id"` // 平台订单号 - ChannelType int `json:"channel_type"` // 支付渠道枚举,1微信JSAPI,2微信H5,3微信app,4微信Native,5微信小程序,6支付宝网页&移动应用,7支付宝小程序,8支付宝JSAPI - Description string `json:"description"` // 商品描述 - Amount int `json:"amount"` // 金额单位为分 - PayerClientIp string `json:"payer_client_ip"` // 终端IP - ReturnUrl string `json:"return_url"` // 支付成功后回调地址 - Wx WxPay `json:"wx"` // 微信支付参数 - Ali AliPay `json:"ali"` // 支付宝参数 - OpenId string `json:"open_id"` // 支付者信息。 openid + PayChannelId int64 `json:"pay_channel_id"` // 支付方式ID + OrderId int64 `json:"order_id"` // 平台订单号 + ChannelType int `json:"channel_type"` // 支付渠道枚举,1微信JSAPI,2微信H5,3微信app,4微信Native,5微信小程序,6支付宝网页&移动应用,7支付宝小程序,8支付宝JSAPI + Description string `json:"description"` // 商品描述 + Amount int `json:"amount"` // 金额单位为分 + PayerClientIp string `json:"payer_client_ip"` // 终端IP + ReturnUrl string `json:"return_url"` // 支付成功后回调地址 + Wx WxPay `json:"wx"` // 微信支付参数 + Ali AliPay `json:"ali"` // 支付宝参数 + Psbc PsbcPay `json:"psbc"` // 邮储支付参数 + OpenId string `json:"open_id"` // 支付者信息。 openid } type WxPay struct { @@ -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{ @@ -109,10 +113,11 @@ func PaymentService(c context.Context, payOrderRequest PayOrderRequest) PayOrder } type PayOrderQueryRequest struct { - OrderId int64 `json:"order_id"` // 商户订单号 - PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb - Wx WxPay `json:"wx"` - Ali AliPay `json:"ali"` + OrderId int64 `json:"order_id"` // 商户订单号 + 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, @@ -177,13 +184,15 @@ func PayOrderQuery(c context.Context, payOrderQueryRequest PayOrderQueryRequest) } type OrderRefundRequest struct { - OrderId int64 `json:"order_id"` // 订单编号 - RefundOrderId int64 `json:"refund_order_id"` // 退款订单号 - RefundReason string `json:"refund_reason"` // 退款原因 - RefundAmount int64 `json:"refund_amount"` // 退款金额,单位分, 微信需要传入订单总额,不是用户实际支付金额 - PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb - Wx WxPay `json:"wx"` - Ali AliPay `json:"ali"` + OrderId int64 `json:"order_id"` // 订单编号 + RefundOrderId int64 `json:"refund_order_id"` // 退款订单号 + 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, @@ -247,11 +258,12 @@ func OrderRefund(c context.Context, orderRefundRequest OrderRefundRequest) Order } type OrderRefundQueryRequest struct { - RefundOrderId int64 `json:"refund_order_id"` // 退款订单号 - OrderId int64 `json:"order_id"` // 支付订单号 - PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb - Wx WxPay `json:"wx"` - Ali AliPay `json:"ali"` + RefundOrderId int64 `json:"refund_order_id"` // 退款订单号 + OrderId int64 `json:"order_id"` // 支付订单号 + 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"` @@ -304,10 +316,11 @@ func OrderRefundQuery(c context.Context, orderRefundQueryRequest OrderRefundQuer } type OrderCloseRequest struct { - OrderId int64 `json:"order_id"` // 支付订单号 - PayChannel int `json:"pay_channel"` // 支付渠道类型 1:wx,2:zfb - Wx WxPay `json:"wx"` - Ali AliPay `json:"ali"` + OrderId int64 `json:"order_id"` // 支付订单号 + 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, diff --git a/app/third/paymentService/psbc/README.md b/app/third/paymentService/psbc/README.md new file mode 100644 index 0000000..1c4a074 --- /dev/null +++ b/app/third/paymentService/psbc/README.md @@ -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` 切换测试环境地址和密钥。 diff --git a/app/third/paymentService/psbc/bill.go b/app/third/paymentService/psbc/bill.go new file mode 100644 index 0000000..db95ae0 --- /dev/null +++ b/app/third/paymentService/psbc/bill.go @@ -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 +} diff --git a/app/third/paymentService/psbc/client.go b/app/third/paymentService/psbc/client.go new file mode 100644 index 0000000..c0cc074 --- /dev/null +++ b/app/third/paymentService/psbc/client.go @@ -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 +} diff --git a/app/third/paymentService/psbc/encrypt.go b/app/third/paymentService/psbc/encrypt.go new file mode 100644 index 0000000..cb51672 --- /dev/null +++ b/app/third/paymentService/psbc/encrypt.go @@ -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 +} diff --git a/app/third/paymentService/psbc/notify.go b/app/third/paymentService/psbc/notify.go new file mode 100644 index 0000000..4cfb08a --- /dev/null +++ b/app/third/paymentService/psbc/notify.go @@ -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 +} diff --git a/app/third/paymentService/psbc/order.go b/app/third/paymentService/psbc/order.go new file mode 100644 index 0000000..1575173 --- /dev/null +++ b/app/third/paymentService/psbc/order.go @@ -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 +} diff --git a/app/third/paymentService/psbc/pay_url.go b/app/third/paymentService/psbc/pay_url.go new file mode 100644 index 0000000..015e910 --- /dev/null +++ b/app/third/paymentService/psbc/pay_url.go @@ -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) +} diff --git a/app/third/paymentService/psbc/refund.go b/app/third/paymentService/psbc/refund.go new file mode 100644 index 0000000..6c527c5 --- /dev/null +++ b/app/third/paymentService/psbc/refund.go @@ -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 +} diff --git a/app/third/paymentService/psbc/types.go b/app/third/paymentService/psbc/types.go new file mode 100644 index 0000000..34e29b2 --- /dev/null +++ b/app/third/paymentService/psbc/types.go @@ -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" +) diff --git a/app/third/paymentService/psbc_service.go b/app/third/paymentService/psbc_service.go new file mode 100644 index 0000000..a8e0af9 --- /dev/null +++ b/app/third/paymentService/psbc_service.go @@ -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 +} diff --git a/app/utils/util.go b/app/utils/util.go index abe75e3..e63e47e 100644 --- a/app/utils/util.go +++ b/app/utils/util.go @@ -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() diff --git a/logs/.gitignore b/logs/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore