645 lines
19 KiB
Go
645 lines
19 KiB
Go
package controllers
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/models"
|
||
"server/pkg/jwtutil"
|
||
"server/services/payment"
|
||
|
||
"github.com/beego/beego/v2/client/orm"
|
||
beelog "github.com/beego/beego/v2/core/logs"
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
// PlatformPaymentController 平台端支付管理(渠道配置 / 支付订单 / 兜底退回 / 渠道回调)
|
||
type PlatformPaymentController struct {
|
||
beego.Controller
|
||
}
|
||
|
||
func (c *PlatformPaymentController) platformClaims() (*jwtutil.Claims, error) {
|
||
return paymentPlatformClaims(&c.Controller)
|
||
}
|
||
|
||
// paymentPlatformClaims 平台端鉴权(支付相关控制器共用),传各控制器内嵌的 &c.Controller
|
||
func paymentPlatformClaims(c *beego.Controller) (*jwtutil.Claims, error) {
|
||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||
if auth == "" {
|
||
return nil, fmt.Errorf("未登录")
|
||
}
|
||
parts := strings.SplitN(auth, " ", 2)
|
||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||
return nil, fmt.Errorf("认证信息格式错误")
|
||
}
|
||
claims, err := jwtutil.ParseToken(parts[1])
|
||
if err != nil {
|
||
return nil, fmt.Errorf("无效的token")
|
||
}
|
||
if claims.UserType != "platform" {
|
||
return nil, fmt.Errorf("无权访问")
|
||
}
|
||
return claims, nil
|
||
}
|
||
|
||
func (c *PlatformPaymentController) jsonErr(httpStatus, bizCode int, msg string) {
|
||
c.Ctx.Output.SetStatus(httpStatus)
|
||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
func (c *PlatformPaymentController) ok(data interface{}) {
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
// channelSecretKeys 各渠道的敏感参数键(接口回显时只回掩码;提交掩码/空串表示不修改)
|
||
var channelSecretKeys = map[string][]string{
|
||
payment.ChannelWechat: {"api_v3_key"},
|
||
payment.ChannelAlipay: {"app_private_key"},
|
||
payment.ChannelUnionPay: {"cert_password"},
|
||
payment.ChannelCloudPay: {"cert_password"},
|
||
payment.ChannelPayPal: {"client_secret"},
|
||
}
|
||
|
||
func channelRowDTO(row *models.PlatformPaymentChannel) map[string]interface{} {
|
||
hasConfig := (row.ConfigJSON != nil && *row.ConfigJSON != "" && *row.ConfigJSON != "{}") || row.MerchantNo != ""
|
||
if row.Channel == payment.ChannelCloudPay {
|
||
hasConfig = true
|
||
}
|
||
status := "unconfigured"
|
||
if row.Enabled == 1 {
|
||
status = "enabled"
|
||
} else if hasConfig {
|
||
status = "disabled"
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"id": row.ID, "channel": row.Channel, "name": row.Name,
|
||
"merchant_no": row.MerchantNo, "callback_url": row.CallbackURL,
|
||
"enabled": row.Enabled == 1,
|
||
"status": status,
|
||
"has_config": hasConfig,
|
||
"remark": row.Remark,
|
||
"last_test_time": row.LastTestTime, "last_test_result": row.LastTestResult,
|
||
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||
}
|
||
}
|
||
|
||
// maskedConfigView 解密配置并掩码敏感字段,供前端回显
|
||
func maskedConfigView(cfg *payment.ChannelConfig, row *models.PlatformPaymentChannel) map[string]interface{} {
|
||
view := map[string]interface{}{}
|
||
for k, v := range cfg.Params {
|
||
view[k] = v
|
||
}
|
||
for _, key := range channelSecretKeys[row.Channel] {
|
||
if v, ok := view[key]; ok {
|
||
if s, ok2 := v.(string); ok2 && s != "" {
|
||
view[key] = payment.MaskSecret(s)
|
||
}
|
||
}
|
||
}
|
||
return view
|
||
}
|
||
|
||
func findChannelRow(channel string) (*models.PlatformPaymentChannel, error) {
|
||
row := &models.PlatformPaymentChannel{}
|
||
err := models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||
Filter("channel", channel).
|
||
Filter("delete_time__isnull", true).
|
||
One(row)
|
||
return row, err
|
||
}
|
||
|
||
type paymentChannelPayload struct {
|
||
Name string `json:"name"`
|
||
MerchantNo string `json:"merchant_no"`
|
||
CallbackURL string `json:"callback_url"`
|
||
Remark string `json:"remark"`
|
||
Config map[string]string `json:"config"`
|
||
Extra map[string]any `json:"extra"`
|
||
}
|
||
|
||
// loadMergedConfig 加载已存配置并把「新填写且非掩码」的值合并进去(用于保存与测试连接共用)
|
||
func loadMergedConfig(channel string, p *paymentChannelPayload) (*payment.ChannelConfig, error) {
|
||
cfg, err := payment.LoadChannelConfig(channel)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if p == nil {
|
||
return cfg, nil
|
||
}
|
||
for k, v := range p.Config {
|
||
v = strings.TrimSpace(v)
|
||
if v == "" || payment.IsMasked(v) {
|
||
continue // 空串 / 掩码 = 保持原值
|
||
}
|
||
cfg.Params[k] = v
|
||
}
|
||
if p.Extra != nil {
|
||
cfg.Extra = p.Extra
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
// ListChannels GET /platform/payment/channels 渠道概览
|
||
func (c *PlatformPaymentController) ListChannels() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
var rows []models.PlatformPaymentChannel
|
||
_, err := models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||
Filter("delete_time__isnull", true).
|
||
OrderBy("id").
|
||
All(&rows)
|
||
if err != nil && err != orm.ErrNoRows {
|
||
c.jsonErr(500, 500, "查询渠道失败: "+err.Error())
|
||
return
|
||
}
|
||
if rows == nil {
|
||
rows = []models.PlatformPaymentChannel{}
|
||
}
|
||
list := make([]map[string]interface{}, 0, len(rows))
|
||
for i := range rows {
|
||
list = append(list, channelRowDTO(&rows[i]))
|
||
}
|
||
c.ok(map[string]interface{}{"list": list, "total": len(list)})
|
||
}
|
||
|
||
// GetChannel GET /platform/payment/channels/:channel 渠道配置详情(敏感字段掩码)
|
||
func (c *PlatformPaymentController) GetChannel() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
channel := c.Ctx.Input.Param(":channel")
|
||
row, err := findChannelRow(channel)
|
||
if err != nil {
|
||
c.jsonErr(404, 404, "渠道不存在")
|
||
return
|
||
}
|
||
cfg, err := payment.LoadChannelConfig(channel)
|
||
if err != nil {
|
||
c.jsonErr(500, 500, err.Error())
|
||
return
|
||
}
|
||
dto := channelRowDTO(row)
|
||
dto["config"] = maskedConfigView(cfg, row)
|
||
dto["extra"] = cfg.Extra
|
||
dto["cert_files"] = cfg.CertPaths
|
||
c.ok(dto)
|
||
}
|
||
|
||
// SaveChannel POST /platform/payment/channels/:channel 保存渠道配置(敏感字段留空/掩码表示不修改)
|
||
func (c *PlatformPaymentController) SaveChannel() {
|
||
claims, err := c.platformClaims()
|
||
if err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
channel := c.Ctx.Input.Param(":channel")
|
||
if _, err := findChannelRow(channel); err != nil {
|
||
c.jsonErr(404, 404, "渠道不存在")
|
||
return
|
||
}
|
||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
var p paymentChannelPayload
|
||
if err := json.Unmarshal(body, &p); err != nil {
|
||
c.jsonErr(400, 400, "参数错误")
|
||
return
|
||
}
|
||
|
||
merged, err := loadMergedConfig(channel, &p)
|
||
if err != nil {
|
||
c.jsonErr(500, 500, err.Error())
|
||
return
|
||
}
|
||
plain, _ := json.Marshal(merged.Params)
|
||
enc, err := payment.EncryptConfig(string(plain))
|
||
if err != nil {
|
||
c.jsonErr(500, 500, "渠道参数加密失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
extraJSON := ""
|
||
if p.Extra != nil {
|
||
b, _ := json.Marshal(p.Extra)
|
||
extraJSON = string(b)
|
||
}
|
||
|
||
merchantNo := strings.TrimSpace(p.MerchantNo)
|
||
if merchantNo == "" && merged != nil {
|
||
switch channel {
|
||
case payment.ChannelPayPal:
|
||
merchantNo = merged.Get("client_id")
|
||
case payment.ChannelWechat:
|
||
merchantNo = merged.Get("mch_id")
|
||
case payment.ChannelAlipay:
|
||
merchantNo = merged.Get("app_id")
|
||
case payment.ChannelUnionPay, payment.ChannelCloudPay:
|
||
merchantNo = merged.Get("mer_id")
|
||
}
|
||
}
|
||
|
||
now := time.Now()
|
||
updates := orm.Params{
|
||
"config_json": enc,
|
||
"merchant_no": merchantNo,
|
||
"update_time": now,
|
||
}
|
||
if p.Remark != "" {
|
||
updates["remark"] = strings.TrimSpace(p.Remark)
|
||
}
|
||
if p.Name != "" {
|
||
updates["name"] = strings.TrimSpace(p.Name)
|
||
}
|
||
if p.CallbackURL != "" {
|
||
updates["callback_url"] = strings.TrimSpace(p.CallbackURL)
|
||
}
|
||
if extraJSON != "" {
|
||
updates["extra_json"] = extraJSON
|
||
}
|
||
if _, err := models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||
Filter("channel", channel).
|
||
Update(updates); err != nil {
|
||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||
return
|
||
}
|
||
_ = claims
|
||
row, _ := findChannelRow(channel)
|
||
c.ok(channelRowDTO(row))
|
||
}
|
||
|
||
// TestChannel POST /platform/payment/channels/:channel/test 测试连接(支持未保存参数)
|
||
func (c *PlatformPaymentController) TestChannel() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
channel := c.Ctx.Input.Param(":channel")
|
||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
var p paymentChannelPayload
|
||
_ = json.Unmarshal(body, &p) // 允许空 body:只测已保存配置
|
||
|
||
merged, err := loadMergedConfig(channel, &p)
|
||
if err != nil {
|
||
c.jsonErr(500, 500, err.Error())
|
||
return
|
||
}
|
||
adapter, err := payment.GetChannelAdapter(channel)
|
||
if err != nil {
|
||
c.jsonErr(400, 400, err.Error())
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
msg, err := adapter.TestConnect(ctx, merged)
|
||
|
||
now := time.Now()
|
||
result := msg
|
||
if err != nil {
|
||
result = err.Error()
|
||
}
|
||
_, _ = models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||
Filter("channel", channel).
|
||
Update(orm.Params{"last_test_time": now, "last_test_result": result, "update_time": now})
|
||
if err != nil {
|
||
c.jsonErr(400, 400, result)
|
||
return
|
||
}
|
||
c.ok(map[string]interface{}{"message": result})
|
||
}
|
||
|
||
// ToggleChannel POST /platform/payment/channels/:channel/status 启用/停用
|
||
func (c *PlatformPaymentController) ToggleChannel() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
channel := c.Ctx.Input.Param(":channel")
|
||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
var p struct {
|
||
Enabled *bool `json:"enabled"`
|
||
}
|
||
if err := json.Unmarshal(body, &p); err != nil || p.Enabled == nil {
|
||
c.jsonErr(400, 400, "参数错误")
|
||
return
|
||
}
|
||
|
||
cfg, err := payment.LoadChannelConfig(channel)
|
||
if err != nil {
|
||
c.jsonErr(404, 404, "渠道不存在")
|
||
return
|
||
}
|
||
if *p.Enabled && len(cfg.Params) == 0 {
|
||
c.jsonErr(400, 400, "渠道参数未配置,无法启用")
|
||
return
|
||
}
|
||
if _, err := models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||
Filter("channel", channel).
|
||
Update(orm.Params{"enabled": b2i(*p.Enabled), "update_time": time.Now()}); err != nil {
|
||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||
return
|
||
}
|
||
c.ok(map[string]interface{}{"enabled": *p.Enabled})
|
||
}
|
||
|
||
func b2i(v bool) int8 {
|
||
if v {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// UploadCertificate POST /platform/payment/channels/:channel/certificate 上传渠道证书
|
||
// 证书文件保存在服务端 certs/payment/<channel>/ 下(0600 权限,不对外提供访问),
|
||
// 文件路径写入 cert_json,密钥内容绝不入库、不回显。
|
||
func (c *PlatformPaymentController) UploadCertificate() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
channel := c.Ctx.Input.Param(":channel")
|
||
if _, err := findChannelRow(channel); err != nil {
|
||
c.jsonErr(404, 404, "渠道不存在")
|
||
return
|
||
}
|
||
certType := strings.TrimSpace(c.GetString("cert_type"))
|
||
if certType == "" {
|
||
certType = "cert"
|
||
}
|
||
file, header, err := c.GetFile("file")
|
||
if err != nil || header == nil {
|
||
c.jsonErr(400, 400, "请选择证书文件")
|
||
return
|
||
}
|
||
defer func() { _ = file.Close() }()
|
||
|
||
dir := filepath.Join("certs", "payment", channel)
|
||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||
c.jsonErr(500, 500, "创建证书目录失败: "+err.Error())
|
||
return
|
||
}
|
||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||
if ext == "" {
|
||
ext = ".pem"
|
||
}
|
||
name := fmt.Sprintf("%s_%d%s", certType, time.Now().UnixNano(), ext)
|
||
savePath := filepath.Join(dir, name)
|
||
|
||
out, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||
if err != nil {
|
||
c.jsonErr(500, 500, "保存证书失败: "+err.Error())
|
||
return
|
||
}
|
||
if _, err := io.Copy(out, file); err != nil {
|
||
_ = out.Close()
|
||
c.jsonErr(500, 500, "保存证书失败: "+err.Error())
|
||
return
|
||
}
|
||
_ = out.Close()
|
||
|
||
// 更新 cert_json
|
||
cfg, err := payment.LoadChannelConfig(channel)
|
||
if err != nil {
|
||
c.jsonErr(500, 500, err.Error())
|
||
return
|
||
}
|
||
if cfg.CertPaths == nil {
|
||
cfg.CertPaths = map[string]string{}
|
||
}
|
||
certKey := certType + "_path"
|
||
cfg.CertPaths[certKey] = savePath
|
||
if _, err := os.Stat(savePath); err == nil {
|
||
// 同步补充对应关系:cert -> cert_path,key -> key_path
|
||
if certType == "cert" {
|
||
cfg.CertPaths["cert_path"] = savePath
|
||
} else {
|
||
cfg.CertPaths["key_path"] = savePath
|
||
}
|
||
}
|
||
certJSON, _ := json.Marshal(cfg.CertPaths)
|
||
if _, err := models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||
Filter("channel", channel).
|
||
Update(orm.Params{"cert_json": string(certJSON), "update_time": time.Now()}); err != nil {
|
||
c.jsonErr(500, 500, "更新证书信息失败: "+err.Error())
|
||
return
|
||
}
|
||
c.ok(map[string]interface{}{"file": header.Filename, "path": savePath, "cert_type": certType})
|
||
}
|
||
|
||
/* ---------------- 支付订单 ---------------- */
|
||
|
||
func paymentOrderDTO(row *models.PlatformPaymentOrder) map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"id": row.ID, "pay_no": row.PayNo, "out_trade_no": row.OutTradeNo,
|
||
"order_type": row.OrderType, "subject": row.Subject,
|
||
"tenant_id": row.TenantID, "tenant_name": row.TenantName,
|
||
"amount": row.Amount, "order_amount": row.OrderAmount, "currency": row.Currency,
|
||
"channel": row.Channel, "merchant_no": row.MerchantNo, "channel_trade_no": row.ChannelTradeNo,
|
||
"status": row.Status, "refund_amount": row.RefundAmount,
|
||
"return_url": row.ReturnURL, "notify_url": row.NotifyURL,
|
||
"expire_at": row.ExpireAt, "paid_at": row.PaidAt, "closed_at": row.ClosedAt,
|
||
"notify_at": row.NotifyAt, "last_query_at": row.LastQueryAt,
|
||
"order_synced": row.OrderSynced,
|
||
"promoter_id": row.PromoterID, "promoter_name": row.PromoterName, "promoter_type": row.PromoterType,
|
||
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||
}
|
||
}
|
||
|
||
// ListOrders GET /platform/payment/orders 支付单列表
|
||
func (c *PlatformPaymentController) ListOrders() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
page, _ := strconv.Atoi(c.GetString("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.GetString("pageSize", "10"))
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
if pageSize <= 0 || pageSize > 200 {
|
||
pageSize = 10
|
||
}
|
||
|
||
qs := models.Orm.QueryTable(new(models.PlatformPaymentOrder)).Filter("delete_time__isnull", true)
|
||
if v := strings.TrimSpace(c.GetString("tenant_id")); v != "" {
|
||
qs = qs.Filter("tenant_id", v)
|
||
}
|
||
if v := strings.TrimSpace(c.GetString("out_trade_no")); v != "" {
|
||
qs = qs.Filter("out_trade_no__icontains", v)
|
||
}
|
||
if v := strings.TrimSpace(c.GetString("pay_no")); v != "" {
|
||
qs = qs.Filter("pay_no__icontains", v)
|
||
}
|
||
if v := strings.TrimSpace(c.GetString("channel")); v != "" {
|
||
qs = qs.Filter("channel", v)
|
||
}
|
||
if v := strings.TrimSpace(c.GetString("status")); v != "" {
|
||
qs = qs.Filter("status", v)
|
||
}
|
||
if v := strings.TrimSpace(c.GetString("start_time")); v != "" {
|
||
if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil {
|
||
qs = qs.Filter("create_time__gte", t)
|
||
}
|
||
}
|
||
if v := strings.TrimSpace(c.GetString("end_time")); v != "" {
|
||
if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil {
|
||
qs = qs.Filter("create_time__lt", t.AddDate(0, 0, 1))
|
||
}
|
||
}
|
||
|
||
total, err := qs.Count()
|
||
if err != nil {
|
||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||
return
|
||
}
|
||
var rows []models.PlatformPaymentOrder
|
||
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
|
||
if err != nil && err != orm.ErrNoRows {
|
||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||
return
|
||
}
|
||
list := make([]map[string]interface{}, 0, len(rows))
|
||
for i := range rows {
|
||
list = append(list, paymentOrderDTO(&rows[i]))
|
||
}
|
||
c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||
}
|
||
|
||
// OrderDetail GET /platform/payment/orders/:payNo 支付单详情(含状态流转 + 最近回调原文)
|
||
func (c *PlatformPaymentController) OrderDetail() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
payNo := c.Ctx.Input.Param(":payNo")
|
||
row, err := payment.GetOrderByPayNo(payNo)
|
||
if err != nil {
|
||
c.jsonErr(404, 404, "支付单不存在")
|
||
return
|
||
}
|
||
dto := paymentOrderDTO(row)
|
||
|
||
// 状态流转记录
|
||
var logs []models.PlatformPaymentOrderLog
|
||
_, _ = models.Orm.QueryTable(new(models.PlatformPaymentOrderLog)).
|
||
Filter("pay_no", payNo).
|
||
Filter("delete_time__isnull", true).
|
||
OrderBy("id").
|
||
All(&logs)
|
||
records := make([]map[string]interface{}, 0, len(logs))
|
||
for i := range logs {
|
||
records = append(records, map[string]interface{}{
|
||
"to_status": logs[i].ToStatus, "operator": logs[i].Operator,
|
||
"remark": logs[i].Remark, "create_time": logs[i].CreateTime,
|
||
})
|
||
}
|
||
dto["records"] = records
|
||
|
||
// 最近一次成功回调的原文(来自 yz_platform_payment_callback_log.raw_body)
|
||
var cb models.PlatformPaymentCallbackLog
|
||
err = models.Orm.QueryTable(new(models.PlatformPaymentCallbackLog)).
|
||
Filter("pay_no", payNo).
|
||
Filter("delete_time__isnull", true).
|
||
OrderBy("-id").
|
||
One(&cb)
|
||
if err == nil && cb.RawBody != nil {
|
||
dto["callback_raw"] = *cb.RawBody
|
||
dto["notify_at"] = cb.CreateTime
|
||
}
|
||
c.ok(dto)
|
||
}
|
||
|
||
// QueryOrder POST /platform/payment/orders/:payNo/query 手动查询渠道状态(兜底回调丢失)
|
||
func (c *PlatformPaymentController) QueryOrder() {
|
||
if _, err := c.platformClaims(); err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
payNo := c.Ctx.Input.Param(":payNo")
|
||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
var p struct {
|
||
Sync *bool `json:"sync"`
|
||
}
|
||
_ = json.Unmarshal(body, &p)
|
||
sync := true
|
||
if p.Sync != nil {
|
||
sync = *p.Sync
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
row, err := payment.QueryPayment(ctx, payNo, sync)
|
||
if err != nil {
|
||
c.jsonErr(404, 404, err.Error())
|
||
return
|
||
}
|
||
c.ok(paymentOrderDTO(row))
|
||
}
|
||
|
||
// Refund POST /platform/payment/refund 手动原路退回(兜底能力)
|
||
func (c *PlatformPaymentController) Refund() {
|
||
claims, err := c.platformClaims()
|
||
if err != nil {
|
||
c.jsonErr(401, 401, err.Error())
|
||
return
|
||
}
|
||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
var p struct {
|
||
PayNo string `json:"pay_no"`
|
||
Amount int64 `json:"amount"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
if err := json.Unmarshal(body, &p); err != nil || strings.TrimSpace(p.PayNo) == "" {
|
||
c.jsonErr(400, 400, "参数错误")
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||
defer cancel()
|
||
refund, err := payment.ManualRefund(ctx, payment.RefundInput{
|
||
PayNo: strings.TrimSpace(p.PayNo),
|
||
Amount: p.Amount,
|
||
Reason: strings.TrimSpace(p.Reason),
|
||
OperatorID: fmt.Sprintf("%d", claims.UserID),
|
||
OperatorName: claims.Username,
|
||
})
|
||
if err != nil {
|
||
c.jsonErr(400, 400, err.Error())
|
||
return
|
||
}
|
||
c.ok(map[string]interface{}{
|
||
"refund_no": refund.RefundNo, "status": refund.Status,
|
||
"amount": refund.Amount, "channel_refund_no": refund.ChannelRefundNo,
|
||
"fail_reason": refund.FailReason,
|
||
})
|
||
}
|
||
|
||
// Notify POST /platform/payment/callback/:channel 渠道异步通知入口(渠道服务器调用,无平台鉴权)
|
||
// 处理成功时按渠道约定返回:微信 {"code":"SUCCESS"} / 支付宝 success / 银联与 PayPal ok / 200 空体。
|
||
func (c *PlatformPaymentController) Notify() {
|
||
channel := c.Ctx.Input.Param(":channel")
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
ack, err := payment.HandleNotify(ctx, channel, c.Ctx.Request, c.Ctx.Input.IP())
|
||
if err != nil {
|
||
beelog.Warn("支付回调处理失败: channel=%s err=%v", channel, err)
|
||
c.Ctx.Output.SetStatus(http.StatusInternalServerError)
|
||
_, _ = c.Ctx.ResponseWriter.Write([]byte("FAIL"))
|
||
return
|
||
}
|
||
if ack == "" {
|
||
ack = "ok"
|
||
}
|
||
c.Ctx.Output.SetStatus(http.StatusOK)
|
||
_, _ = c.Ctx.ResponseWriter.Write([]byte(ack))
|
||
}
|