增加支付功能

This commit is contained in:
2026-09-15 12:46:29 +08:00
parent 943c3708b0
commit a82b300b1a
46 changed files with 11675 additions and 21 deletions
+214
View File
@@ -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,
})
}
+427
View File
@@ -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})
}
+615
View File
@@ -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})
}