增加支付功能
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services/payment"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendPaymentController 租户端支付接口(下单页 / 收银台)
|
||||
// 租户端身份为 jwt UserType=backend,TenantId 即租户ID。
|
||||
type BackendPaymentController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendPaymentController) backendClaims() (*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 != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendPaymentController) 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 *BackendPaymentController) ok(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendPaymentController) tenantID() string {
|
||||
claims, _ := c.backendClaims()
|
||||
if claims == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(claims.TenantId)
|
||||
}
|
||||
|
||||
// tenantName 查询租户名称(下单快照用)
|
||||
func (c *BackendPaymentController) tenantName() string {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil || claims.TenantId <= 0 {
|
||||
return ""
|
||||
}
|
||||
tenant := &models.SystemTenant{}
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", uint64(claims.TenantId)).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(tenant); err != nil {
|
||||
return ""
|
||||
}
|
||||
return tenant.TenantName
|
||||
}
|
||||
|
||||
// Channels GET /backend/payment/channels 收银台可用渠道(已启用且参数完整)
|
||||
func (c *BackendPaymentController) Channels() {
|
||||
if _, err := c.backendClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.PlatformPaymentChannel
|
||||
_, err := models.Orm.QueryTable(new(models.PlatformPaymentChannel)).
|
||||
Filter("enabled", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("id").
|
||||
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 {
|
||||
cfg, err := payment.LoadChannelConfig(rows[i].Channel)
|
||||
if err != nil || len(cfg.Params) == 0 {
|
||||
continue // 参数不完整的渠道不下发
|
||||
}
|
||||
extra := map[string]interface{}{}
|
||||
for k, v := range cfg.Extra {
|
||||
extra[k] = v
|
||||
}
|
||||
list = append(list, map[string]interface{}{
|
||||
"channel": rows[i].Channel, "name": rows[i].Name, "extra": extra,
|
||||
})
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create POST /backend/payment/create 创建支付单并返回渠道支付参数
|
||||
// 入参:{ out_trade_no, order_type, amount(分), channel, subject, return_url, pay_type, openid,
|
||||
//
|
||||
// promoter_id?, promoter_name?, promoter_type?, expire_minutes? }
|
||||
//
|
||||
// 出参:{ pay_no, channel, pay_params }
|
||||
func (c *BackendPaymentController) Create() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
OrderType string `json:"order_type"`
|
||||
Amount int64 `json:"amount"`
|
||||
Channel string `json:"channel"`
|
||||
Subject string `json:"subject"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
PayType string `json:"pay_type"`
|
||||
OpenID string `json:"openid"`
|
||||
PromoterID string `json:"promoter_id"`
|
||||
PromoterName string `json:"promoter_name"`
|
||||
PromoterType string `json:"promoter_type"`
|
||||
ExpireMinutes int `json:"expire_minutes"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.OutTradeNo) == "" {
|
||||
c.jsonErr(400, 400, "业务订单号不能为空")
|
||||
return
|
||||
}
|
||||
if p.Amount <= 0 {
|
||||
c.jsonErr(400, 400, "支付金额必须大于 0")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Channel) == "" {
|
||||
c.jsonErr(400, 400, "请选择支付渠道")
|
||||
return
|
||||
}
|
||||
if p.OrderType == "" {
|
||||
p.OrderType = models.CommissionOrderTypePlatformUsage
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
order, params, err := payment.CreatePayment(ctx, payment.CreateInput{
|
||||
OutTradeNo: strings.TrimSpace(p.OutTradeNo),
|
||||
OrderType: p.OrderType,
|
||||
TenantID: strconv.Itoa(claims.TenantId),
|
||||
TenantName: c.tenantName(),
|
||||
Amount: p.Amount,
|
||||
Channel: strings.TrimSpace(p.Channel),
|
||||
Subject: strings.TrimSpace(p.Subject),
|
||||
ReturnURL: strings.TrimSpace(p.ReturnURL),
|
||||
ClientIP: c.Ctx.Input.IP(),
|
||||
PayType: p.PayType,
|
||||
OpenID: p.OpenID,
|
||||
PromoterID: strings.TrimSpace(p.PromoterID),
|
||||
PromoterName: strings.TrimSpace(p.PromoterName),
|
||||
PromoterType: strings.TrimSpace(p.PromoterType),
|
||||
ExpireMinutes: p.ExpireMinutes,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{
|
||||
"pay_no": order.PayNo,
|
||||
"channel": order.Channel,
|
||||
"status": order.Status,
|
||||
"pay_params": params,
|
||||
})
|
||||
}
|
||||
|
||||
// Query GET /backend/payment/query/:payNo 查询支付单(仅本租户)
|
||||
func (c *BackendPaymentController) Query() {
|
||||
if _, err := c.backendClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
payNo := c.Ctx.Input.Param(":payNo")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
row, err := payment.QueryPayment(ctx, payNo, true)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "支付单不存在")
|
||||
return
|
||||
}
|
||||
if row.TenantID != c.tenantID() {
|
||||
c.jsonErr(403, 403, "无权访问该支付单")
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{
|
||||
"pay_no": row.PayNo, "out_trade_no": row.OutTradeNo,
|
||||
"amount": row.Amount, "channel": row.Channel, "status": row.Status,
|
||||
"paid_at": row.PaidAt, "create_time": row.CreateTime,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services/payment"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformCommissionController 平台端推广佣金(台账 / 统计 / 规则维护)
|
||||
type PlatformCommissionController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformCommissionController) platformClaims() (*jwtutil.Claims, error) {
|
||||
return paymentPlatformClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *PlatformCommissionController) 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 *PlatformCommissionController) ok(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// commissionRuleDTO 规则出参。
|
||||
// 比例值统一按「百分比」返回:库里存百分比×100(500 = 5%),接口返回 5,便于前端直接编辑。
|
||||
func commissionRuleDTO(row *models.PlatformCommissionRule) map[string]interface{} {
|
||||
dto := map[string]interface{}{
|
||||
"id": row.ID, "rule_name": row.RuleName, "calc_type": row.CalcType,
|
||||
"order_type": row.OrderType, "status": row.Status, "remark": row.Remark,
|
||||
"create_user_id": row.CreateUserID, "create_user_name": row.CreateUserName,
|
||||
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||
"steps": []map[string]interface{}{},
|
||||
}
|
||||
if row.CalcType == models.CommissionCalcFixed {
|
||||
dto["value"] = row.Value // 固定金额:分
|
||||
} else {
|
||||
dto["value"] = float64(row.Value) / 100 // 比例:5% -> 5
|
||||
}
|
||||
if row.CalcType == models.CommissionCalcTiered && row.TierJSON != nil && *row.TierJSON != "" {
|
||||
var tiers []models.CommissionTier
|
||||
if err := json.Unmarshal([]byte(*row.TierJSON), &tiers); err == nil {
|
||||
steps := make([]map[string]interface{}, 0, len(tiers))
|
||||
for _, t := range tiers {
|
||||
var maxV interface{}
|
||||
if t.Max != nil {
|
||||
maxV = *t.Max
|
||||
}
|
||||
steps = append(steps, map[string]interface{}{
|
||||
"min": t.Min, "max": maxV, "rate": float64(t.Rate) / 100,
|
||||
})
|
||||
}
|
||||
dto["steps"] = steps
|
||||
}
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
// commissionOrderDTO 佣金单出参
|
||||
func commissionOrderDTO(row *models.PlatformCommissionOrder) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID, "commission_no": row.CommissionNo,
|
||||
"pay_no": row.PayNo, "out_trade_no": row.OutTradeNo,
|
||||
"rule_id": row.RuleID, "rule_name": row.RuleName, "calc_type": row.CalcType,
|
||||
"promoter_id": row.PromoterID, "promoter_name": row.PromoterName, "promoter_type": row.PromoterType,
|
||||
"base_amount": row.BaseAmount, "amount": row.Amount,
|
||||
"status": row.Status, "pay_method": row.PayMethod, "pay_voucher": row.PayVoucher,
|
||||
"paid_at": row.PaidAt, "paid_user_id": row.PaidUserID, "paid_user_name": row.PaidUserName,
|
||||
"remark": row.Remark, "create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||
}
|
||||
}
|
||||
|
||||
// List GET /platform/commission/list 佣金台账
|
||||
func (c *PlatformCommissionController) List() {
|
||||
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"))
|
||||
rows, total, err := payment.ListCommissions(payment.CommissionListInput{
|
||||
Promoter: strings.TrimSpace(c.GetString("promoter")),
|
||||
Status: strings.TrimSpace(c.GetString("status")),
|
||||
StartTime: strings.TrimSpace(c.GetString("start_time")),
|
||||
EndTime: strings.TrimSpace(c.GetString("end_time")),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.PlatformCommissionOrder{}
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
list = append(list, commissionOrderDTO(&rows[i]))
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// Stats GET /platform/commission/stats 台账统计
|
||||
func (c *PlatformCommissionController) Stats() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
stats, err := payment.GetCommissionStats()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "统计失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(stats)
|
||||
}
|
||||
|
||||
// MarkPaid POST /platform/commission/:id/pay 标记已付(发放登记)
|
||||
func (c *PlatformCommissionController) MarkPaid() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
PayMethod string `json:"pay_method"`
|
||||
PayVoucher string `json:"pay_voucher"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.PayMethod) == "" {
|
||||
c.jsonErr(400, 400, "请选择发放方式")
|
||||
return
|
||||
}
|
||||
row, err := payment.MarkCommissionPaid(context.Background(), payment.MarkCommissionPaidInput{
|
||||
ID: id, PayMethod: strings.TrimSpace(p.PayMethod), PayVoucher: strings.TrimSpace(p.PayVoucher),
|
||||
Remark: strings.TrimSpace(p.Remark),
|
||||
OperatorID: fmt.Sprintf("%d", claims.UserID), OperatorName: claims.Username,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(commissionOrderDTO(row))
|
||||
}
|
||||
|
||||
/* ---------------- 佣金规则 ---------------- */
|
||||
|
||||
type commissionRulePayload struct {
|
||||
RuleName string `json:"rule_name"`
|
||||
CalcType string `json:"calc_type"`
|
||||
Value *float64 `json:"value"`
|
||||
Steps []struct {
|
||||
Min *float64 `json:"min"`
|
||||
Max *float64 `json:"max"`
|
||||
Rate *float64 `json:"rate"`
|
||||
} `json:"steps"`
|
||||
OrderType string `json:"order_type"`
|
||||
Status *int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// buildTierJSON 组装阶梯配置(金额分;rate 存百分比×100)
|
||||
func buildTierJSON(steps []struct {
|
||||
Min *float64 `json:"min"`
|
||||
Max *float64 `json:"max"`
|
||||
Rate *float64 `json:"rate"`
|
||||
}) (string, error) {
|
||||
if len(steps) == 0 {
|
||||
return "", fmt.Errorf("阶梯规则至少需要一档")
|
||||
}
|
||||
tiers := make([]models.CommissionTier, 0, len(steps))
|
||||
for i, s := range steps {
|
||||
if s.Min == nil || s.Rate == nil {
|
||||
return "", fmt.Errorf("第 %d 档的起始金额与比例必填", i+1)
|
||||
}
|
||||
tier := models.CommissionTier{
|
||||
Min: int64(*s.Min + 0.5),
|
||||
Rate: int64(*s.Rate*100 + 0.5),
|
||||
}
|
||||
if s.Max != nil && *s.Max > 0 {
|
||||
mx := int64(*s.Max + 0.5)
|
||||
tier.Max = &mx
|
||||
if mx <= tier.Min {
|
||||
return "", fmt.Errorf("第 %d 档结束金额需大于起始金额", i+1)
|
||||
}
|
||||
}
|
||||
if tier.Rate <= 0 {
|
||||
return "", fmt.Errorf("第 %d 档比例需大于 0", i+1)
|
||||
}
|
||||
tiers = append(tiers, tier)
|
||||
}
|
||||
b, err := json.Marshal(tiers)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// RulesList GET /platform/commission/rules 规则列表
|
||||
func (c *PlatformCommissionController) RulesList() {
|
||||
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", "20"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 200 {
|
||||
pageSize = 20
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.PlatformCommissionRule)).Filter("delete_time__isnull", true)
|
||||
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
|
||||
qs = qs.Filter("rule_name__icontains", kw)
|
||||
}
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.PlatformCommissionRule
|
||||
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.PlatformCommissionRule{}
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
list = append(list, commissionRuleDTO(&rows[i]))
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// RuleCreate POST /platform/commission/rules 新增规则
|
||||
func (c *PlatformCommissionController) RuleCreate() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p commissionRulePayload
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.RuleName) == "" {
|
||||
c.jsonErr(400, 400, "规则名称不能为空")
|
||||
return
|
||||
}
|
||||
row := &models.PlatformCommissionRule{
|
||||
RuleName: strings.TrimSpace(p.RuleName),
|
||||
CalcType: strings.TrimSpace(p.CalcType),
|
||||
OrderType: strings.TrimSpace(p.OrderType),
|
||||
Status: 1,
|
||||
Remark: strings.TrimSpace(p.Remark),
|
||||
CreateUserID: fmt.Sprintf("%d", claims.UserID),
|
||||
CreateUserName: claims.Username,
|
||||
}
|
||||
if p.Status != nil {
|
||||
row.Status = *p.Status
|
||||
}
|
||||
if err := applyCommissionRuleValues(row, &p); err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.jsonErr(500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(commissionRuleDTO(row))
|
||||
}
|
||||
|
||||
// RuleUpdate POST /platform/commission/rules/:id 更新规则
|
||||
func (c *PlatformCommissionController) RuleUpdate() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
row := &models.PlatformCommissionRule{}
|
||||
if err := models.Orm.QueryTable(new(models.PlatformCommissionRule)).
|
||||
Filter("id", id).Filter("delete_time__isnull", true).One(row); err != nil {
|
||||
c.jsonErr(404, 404, "规则不存在")
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p commissionRulePayload
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if name := strings.TrimSpace(p.RuleName); name != "" {
|
||||
row.RuleName = name
|
||||
}
|
||||
if calc := strings.TrimSpace(p.CalcType); calc != "" {
|
||||
row.CalcType = calc
|
||||
}
|
||||
if ot := strings.TrimSpace(p.OrderType); ot != "" {
|
||||
row.OrderType = ot
|
||||
}
|
||||
row.Remark = strings.TrimSpace(p.Remark)
|
||||
if p.Status != nil {
|
||||
row.Status = *p.Status
|
||||
}
|
||||
if err := applyCommissionRuleValues(row, &p); err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := models.Orm.Update(row); err != nil {
|
||||
c.jsonErr(500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(commissionRuleDTO(row))
|
||||
}
|
||||
|
||||
// applyCommissionRuleValues 按计算方式写入 value / tier_json
|
||||
func applyCommissionRuleValues(row *models.PlatformCommissionRule, p *commissionRulePayload) error {
|
||||
switch row.CalcType {
|
||||
case models.CommissionCalcPercent:
|
||||
if p.Value == nil || *p.Value <= 0 {
|
||||
return fmt.Errorf("请输入大于 0 的比例值")
|
||||
}
|
||||
row.Value = int64(*p.Value*100 + 0.5)
|
||||
row.TierJSON = nil
|
||||
case models.CommissionCalcFixed:
|
||||
if p.Value == nil || *p.Value <= 0 {
|
||||
return fmt.Errorf("请输入大于 0 的固定金额(分)")
|
||||
}
|
||||
row.Value = int64(*p.Value + 0.5)
|
||||
row.TierJSON = nil
|
||||
case models.CommissionCalcTiered:
|
||||
tierJSON, err := buildTierJSON(p.Steps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row.Value = 0
|
||||
row.TierJSON = &tierJSON
|
||||
default:
|
||||
return fmt.Errorf("计算方式不合法")
|
||||
}
|
||||
switch row.OrderType {
|
||||
case models.CommissionOrderTypeAll, models.CommissionOrderTypePlatformUsage,
|
||||
models.CommissionOrderTypeModuleShop, models.CommissionOrderTypeServiceFee:
|
||||
default:
|
||||
return fmt.Errorf("适用订单类型不合法")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RuleDelete DELETE /platform/commission/rules/:id 删除规则(软删除;历史佣金单不受影响)
|
||||
func (c *PlatformCommissionController) RuleDelete() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if _, err := models.Orm.QueryTable(new(models.PlatformCommissionRule)).
|
||||
Filter("id", id).
|
||||
Update(orm.Params{"delete_time": now, "update_time": now}); err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(nil)
|
||||
}
|
||||
|
||||
// RuleToggle POST /platform/commission/rules/:id/status 启用/停用
|
||||
func (c *PlatformCommissionController) RuleToggle() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
Status *int8 `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil || p.Status == nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if _, err := models.Orm.QueryTable(new(models.PlatformCommissionRule)).
|
||||
Filter("id", id).
|
||||
Update(orm.Params{"status": *p.Status, "update_time": time.Now()}); err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{"status": *p.Status})
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
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{} {
|
||||
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,
|
||||
"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)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
updates := orm.Params{
|
||||
"config_json": enc,
|
||||
"merchant_no": strings.TrimSpace(p.MerchantNo),
|
||||
"remark": strings.TrimSpace(p.Remark),
|
||||
"update_time": now,
|
||||
}
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services/payment"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformPaymentReconcileController 平台端对账管理(差异列表 / 标记处理 / 账单导入 / 执行对账)
|
||||
type PlatformPaymentReconcileController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformPaymentReconcileController) platformClaims() (*jwtutil.Claims, error) {
|
||||
return paymentPlatformClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *PlatformPaymentReconcileController) 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 *PlatformPaymentReconcileController) ok(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func reconcileDiffDTO(row *models.PlatformPaymentReconcileDiff) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID, "channel": row.Channel, "bill_date": row.BillDate,
|
||||
"batch_no": row.BatchNo, "local_trade_no": row.LocalTradeNo, "channel_trade_no": row.ChannelTradeNo,
|
||||
"local_amount": row.LocalAmount, "channel_amount": row.ChannelAmount, "diff_amount": row.DiffAmount,
|
||||
"diff_type": row.DiffType, "handle_status": row.HandleStatus,
|
||||
"handle_user_id": row.HandleUserID, "handle_user_name": row.HandleUserName,
|
||||
"handle_time": row.HandleTime, "remark": row.Remark,
|
||||
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||
}
|
||||
}
|
||||
|
||||
// ListReconcile GET /platform/payment/reconcile 对账差异列表
|
||||
func (c *PlatformPaymentReconcileController) ListReconcile() {
|
||||
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"))
|
||||
rows, total, err := payment.ListReconcileDiffs(payment.ReconcileListInput{
|
||||
Channel: strings.TrimSpace(c.GetString("channel")),
|
||||
BillDate: strings.TrimSpace(c.GetString("bill_date")),
|
||||
DiffType: strings.TrimSpace(c.GetString("diff_type")),
|
||||
HandleStatus: strings.TrimSpace(c.GetString("handle_status")),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.PlatformPaymentReconcileDiff{}
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
list = append(list, reconcileDiffDTO(&rows[i]))
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// HandleReconcile POST /platform/payment/reconcile/:id/handle 标记差异处理
|
||||
func (c *PlatformPaymentReconcileController) HandleReconcile() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
HandleStatus string `json:"handle_status"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
row, err := payment.MarkDiffHandled(context.Background(), payment.MarkDiffHandledInput{
|
||||
ID: id, HandleStatus: strings.TrimSpace(p.HandleStatus), Remark: strings.TrimSpace(p.Remark),
|
||||
OperatorID: fmt.Sprintf("%d", claims.UserID), OperatorName: claims.Username,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(reconcileDiffDTO(row))
|
||||
}
|
||||
|
||||
// RunReconcile POST /platform/payment/reconcile/run 执行对账比对
|
||||
func (c *PlatformPaymentReconcileController) RunReconcile() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
Channel string `json:"channel"`
|
||||
BillDate string `json:"bill_date"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
summary, err := payment.RunReconcile(ctx, strings.TrimSpace(p.Channel), strings.TrimSpace(p.BillDate))
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(summary)
|
||||
}
|
||||
|
||||
// ImportBill POST /platform/payment/reconcile/import 导入渠道账单
|
||||
// 两种方式:multipart 上传 CSV(字段 file + channel + bill_date),或 JSON {channel, bill_date, rows:[...]}
|
||||
func (c *PlatformPaymentReconcileController) ImportBill() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var channel, billDate string
|
||||
var rows []payment.BillRow
|
||||
|
||||
contentType := c.Ctx.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
channel = strings.TrimSpace(c.GetString("channel"))
|
||||
billDate = strings.TrimSpace(c.GetString("bill_date"))
|
||||
file, header, ferr := c.GetFile("file")
|
||||
if ferr != nil || header == nil {
|
||||
c.jsonErr(400, 400, "请选择账单文件")
|
||||
return
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
data, rerr := io.ReadAll(file)
|
||||
if rerr != nil {
|
||||
c.jsonErr(400, 400, "读取账单文件失败: "+rerr.Error())
|
||||
return
|
||||
}
|
||||
rows, _ = payment.ParseChannelBillCSV(data)
|
||||
} else {
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
Channel string `json:"channel"`
|
||||
BillDate string `json:"bill_date"`
|
||||
Rows []payment.BillRow `json:"rows"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
channel, billDate, rows = strings.TrimSpace(p.Channel), strings.TrimSpace(p.BillDate), p.Rows
|
||||
}
|
||||
|
||||
if channel == "" || billDate == "" {
|
||||
c.jsonErr(400, 400, "channel 与 bill_date 不能为空")
|
||||
return
|
||||
}
|
||||
imported, err := payment.ImportChannelBill(context.Background(), channel, billDate, rows)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{"imported": imported, "channel": channel, "bill_date": billDate})
|
||||
}
|
||||
Reference in New Issue
Block a user