支持多产品计划展示、LICOFF 离线签发、支付独立配置与调试,以及分析看板;同步更新 README 与集成文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
502 lines
18 KiB
Go
502 lines
18 KiB
Go
package order
|
|
|
|
import (
|
|
"backend/internal/dto"
|
|
"backend/internal/model"
|
|
orderservice "backend/internal/service/order"
|
|
"backend/utils"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// OrderController 订单控制器 // Logan.Li
|
|
type OrderController struct {
|
|
svc *orderservice.OrderService
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewOrderController 创建控制器实例 // Logan.Li
|
|
func NewOrderController(db *gorm.DB) *OrderController {
|
|
return &OrderController{svc: orderservice.NewOrderService(db), db: db}
|
|
}
|
|
|
|
// List 订单列表(管理后台) // Logan.Li
|
|
func (ctrl *OrderController) List(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
|
status := c.Query("status")
|
|
productID, _ := strconv.ParseUint(c.Query("product_id"), 10, 32)
|
|
|
|
orders, total, err := ctrl.svc.List(page, pageSize, status, uint(productID))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
|
"list": orders, "total": total, "page": page, "page_size": pageSize,
|
|
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
|
}})
|
|
}
|
|
|
|
// GetDetail 订单详情 // Logan.Li
|
|
func (ctrl *OrderController) GetDetail(c *gin.Context) {
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
order, err := ctrl.svc.GetByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 404, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": order})
|
|
}
|
|
|
|
// CreateOrder 客户端下单(公开接口) // Logan.Li
|
|
func (ctrl *OrderController) CreateOrder(c *gin.Context) {
|
|
var req dto.CreateOrderRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
resp, err := ctrl.svc.CreateOrder(req, utils.GetClientIP(c), c.GetHeader("User-Agent"))
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 2001, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "订单创建成功", "data": resp})
|
|
}
|
|
|
|
// Checkout desktop-friendly checkout entry. // Logan.Li
|
|
func (ctrl *OrderController) Checkout(c *gin.Context) {
|
|
var req dto.CheckoutRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
resp, err := ctrl.svc.Checkout(req, utils.GetClientIP(c), c.GetHeader("User-Agent"))
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 2001, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "订单创建成功", "data": resp})
|
|
}
|
|
|
|
// ClaimPurchase fetch license after payment. // Logan.Li
|
|
func (ctrl *OrderController) ClaimPurchase(c *gin.Context) {
|
|
var req dto.ClaimPurchaseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
resp, err := ctrl.svc.ClaimPurchase(req)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 2002, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": resp})
|
|
}
|
|
|
|
// PaymentWebhook 支付回调(公开接口,供支付渠道调用) // Logan.Li
|
|
func (ctrl *OrderController) PaymentWebhook(c *gin.Context) {
|
|
var req dto.PaymentWebhookRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "Invalid payload"})
|
|
return
|
|
}
|
|
if err := ctrl.svc.PaymentWebhook(req, utils.GetClientIP(c)); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "Webhook processed"})
|
|
}
|
|
|
|
// WaffoWebhookReady GET/HEAD — browser-friendly landing (not raw JSON). // Logan.Li
|
|
func (ctrl *OrderController) WaffoWebhookReady(c *gin.Context) {
|
|
orderNo := firstNonEmpty(c.Query("order_no"), c.Query("orderNo"))
|
|
if orderNo != "" {
|
|
// Browser return with orderNo → show success/claim page.
|
|
c.Redirect(http.StatusFound, "/api/v1/purchase/success?"+c.Request.URL.RawQuery)
|
|
return
|
|
}
|
|
if prefersJSON(c) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"message": "waffo webhook ready",
|
|
"methods": []string{"POST"},
|
|
})
|
|
return
|
|
}
|
|
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(renderSimplePage(
|
|
"OpsXShell 支付回调",
|
|
"接口已就绪",
|
|
"这是给支付平台调用的服务端回调地址,请在 Waffo 后台配置为 Webhook(POST)。支付完成后会自动跳转到授权领取页。",
|
|
"",
|
|
"",
|
|
)))
|
|
}
|
|
|
|
// PurchaseSuccess browser success / claim page after Waffo checkout. // Logan.Li
|
|
func (ctrl *OrderController) PurchaseSuccess(c *gin.Context) {
|
|
orderNo := firstNonEmpty(c.Query("order_no"), c.Query("orderNo"))
|
|
email := firstNonEmpty(c.Query("email"), c.Query("customer_email"), c.Query("customerEmail"))
|
|
planType := firstNonEmpty(c.Query("planType"), c.Query("plan_type"))
|
|
|
|
// If Waffo bounced here with paid-like query, try to settle once. // Logan.Li
|
|
statusHint := strings.ToLower(firstNonEmpty(c.Query("status"), c.Query("payment_status"), c.Query("paymentStatus")))
|
|
if orderNo != "" && (strings.Contains(statusHint, "paid") || strings.Contains(statusHint, "success") || strings.Contains(statusHint, "complete")) {
|
|
_ = ctrl.svc.PaymentWebhook(dto.PaymentWebhookRequest{
|
|
EventType: "browser_return",
|
|
OrderNo: orderNo,
|
|
Status: "paid",
|
|
Gateway: "waffo",
|
|
Payload: c.Request.URL.RawQuery,
|
|
}, utils.GetClientIP(c))
|
|
}
|
|
|
|
title := "支付结果"
|
|
headline := "正在确认支付…"
|
|
body := "若你已完成付款,请稍等数秒后刷新本页;或打开 OpsXShell,在「账户」粘贴许可证激活。"
|
|
licenseKey := ""
|
|
orderStatus := ""
|
|
|
|
if orderNo != "" {
|
|
var order model.Order
|
|
if err := ctrl.db.Where("order_no = ?", orderNo).First(&order).Error; err == nil {
|
|
orderStatus = order.Status
|
|
switch order.Status {
|
|
case "paid":
|
|
headline = "支付成功"
|
|
if order.LicenseID > 0 {
|
|
var lic model.License
|
|
if err := ctrl.db.First(&lic, order.LicenseID).Error; err == nil {
|
|
licenseKey = lic.LicenseKey
|
|
}
|
|
}
|
|
if licenseKey == "" {
|
|
claimed, err := ctrl.svc.ClaimPurchase(dto.ClaimPurchaseRequest{
|
|
OrderNo: orderNo,
|
|
CustomerEmail: email,
|
|
ProductCode: "opsxshell",
|
|
})
|
|
if err == nil && claimed != nil {
|
|
licenseKey = claimed.LicenseKey
|
|
}
|
|
}
|
|
if licenseKey != "" {
|
|
body = "请复制下方许可证,回到 OpsXShell → 设置 / 升级页 → 粘贴激活。也可点击「打开应用」自动回跳。"
|
|
} else {
|
|
body = "订单已支付,许可证生成中,请稍后刷新本页。"
|
|
}
|
|
case "pending":
|
|
headline = "等待支付确认"
|
|
body = fmt.Sprintf("订单 %s 尚未收到支付平台回调。若你已付款,通常 1~2 分钟内会自动到账;也可联系客服并提供订单号。", html.EscapeString(orderNo))
|
|
default:
|
|
headline = "订单状态:" + order.Status
|
|
body = "如有疑问请携带订单号联系支持。"
|
|
}
|
|
} else {
|
|
headline = "未找到订单"
|
|
body = "请确认是否从 OpsXShell 内发起购买。订单号:" + html.EscapeString(orderNo)
|
|
}
|
|
} else if email != "" {
|
|
claimed, err := ctrl.svc.ClaimPurchase(dto.ClaimPurchaseRequest{
|
|
CustomerEmail: email,
|
|
ProductCode: "opsxshell",
|
|
})
|
|
if err == nil && claimed != nil && claimed.LicenseKey != "" {
|
|
headline = "支付成功"
|
|
licenseKey = claimed.LicenseKey
|
|
orderNo = claimed.OrderNo
|
|
orderStatus = claimed.Status
|
|
body = "已根据邮箱找到可用许可证,请复制后在 OpsXShell 中激活。"
|
|
}
|
|
}
|
|
|
|
deepLink := "opsxshell://pro/activated"
|
|
q := []string{}
|
|
if orderNo != "" {
|
|
q = append(q, "orderNo="+orderNo)
|
|
}
|
|
if email != "" {
|
|
q = append(q, "email="+email)
|
|
}
|
|
if licenseKey != "" {
|
|
q = append(q, "licenseKey="+licenseKey)
|
|
}
|
|
if planType != "" {
|
|
q = append(q, "planType="+planType)
|
|
}
|
|
if len(q) > 0 {
|
|
deepLink += "?" + strings.Join(q, "&")
|
|
}
|
|
|
|
meta := ""
|
|
if orderNo != "" {
|
|
meta += "<p class=\"meta\">订单号:" + html.EscapeString(orderNo)
|
|
if orderStatus != "" {
|
|
meta += " · 状态:" + html.EscapeString(orderStatus)
|
|
}
|
|
meta += "</p>"
|
|
}
|
|
if email != "" {
|
|
meta += "<p class=\"meta\">邮箱:" + html.EscapeString(email) + "</p>"
|
|
}
|
|
|
|
keyBlock := ""
|
|
if licenseKey != "" {
|
|
keyBlock = `<div class="key-box"><div class="key-label">许可证</div><code id="lic">` + html.EscapeString(licenseKey) + `</code>
|
|
<button type="button" onclick="navigator.clipboard.writeText(document.getElementById('lic').innerText)">复制</button></div>`
|
|
}
|
|
|
|
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(renderSimplePage(
|
|
"OpsXShell · "+title,
|
|
headline,
|
|
body,
|
|
meta+keyBlock,
|
|
deepLink,
|
|
)))
|
|
}
|
|
|
|
func prefersJSON(c *gin.Context) bool {
|
|
accept := c.GetHeader("Accept")
|
|
return strings.Contains(accept, "application/json") && !strings.Contains(accept, "text/html")
|
|
}
|
|
|
|
func renderSimplePage(title, headline, body, extraHTML, deepLink string) string {
|
|
openBtn := ""
|
|
autoScript := ""
|
|
if deepLink != "" {
|
|
openBtn = `<p><a class="btn" href="` + html.EscapeString(deepLink) + `">打开 OpsXShell 激活</a></p>`
|
|
autoScript = `<script>setTimeout(function(){ try{ window.location.href=` + strconv.Quote(deepLink) + `; }catch(e){} }, 800);</script>`
|
|
}
|
|
return `<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
<title>` + html.EscapeString(title) + `</title>
|
|
<style>
|
|
:root { color-scheme: dark; }
|
|
body{margin:0;min-height:100vh;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
background:radial-gradient(1200px 600px at 20% -10%,#1e3a5f 0%,#0b1220 55%,#070b14 100%);color:#e8eef7;display:flex;align-items:center;justify-content:center;padding:24px;}
|
|
.card{width:min(520px,100%);background:rgba(18,26,40,.92);border:1px solid rgba(255,255,255,.08);border-radius:20px;padding:28px 26px;box-shadow:0 24px 60px rgba(0,0,0,.35);}
|
|
.brand{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:#7fb0ff;margin-bottom:10px;}
|
|
h1{font-size:24px;margin:0 0 10px;font-weight:650;}
|
|
p{line-height:1.6;color:#b7c3d6;margin:0 0 12px;}
|
|
.meta{font-size:13px;color:#8a98ad;}
|
|
.key-box{margin:16px 0;padding:14px;border-radius:12px;background:#0a101a;border:1px solid rgba(127,176,255,.25);}
|
|
.key-label{font-size:12px;color:#8a98ad;margin-bottom:6px;}
|
|
code{display:block;font-size:15px;word-break:break-all;color:#f4f7fb;margin-bottom:10px;}
|
|
button,.btn{appearance:none;border:0;border-radius:10px;padding:10px 14px;background:#3b82f6;color:#fff;font-weight:600;cursor:pointer;text-decoration:none;display:inline-block;}
|
|
button:hover,.btn:hover{background:#2563eb;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="brand">OpsXShell</div>
|
|
<h1>` + html.EscapeString(headline) + `</h1>
|
|
<p>` + html.EscapeString(body) + `</p>
|
|
` + extraHTML + `
|
|
` + openBtn + `
|
|
</div>
|
|
` + autoScript + `
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
// WaffoWebhook maps official Waffo Raw JSON webhooks (order.completed etc.) to PaymentWebhook.
|
|
// Prefer data.orderMerchantExternalId (= our ORD-*) for matching; also accept legacy shapes. // Logan.Li
|
|
func (ctrl *OrderController) WaffoWebhook(c *gin.Context) {
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "无法读取请求体"})
|
|
return
|
|
}
|
|
sigHeader := c.GetHeader("X-Waffo-Signature")
|
|
_ = sigHeader // optional RSA verify when WAFFO_WEBHOOK_PUBLIC_KEY is set later // Logan.Li
|
|
|
|
var payload map[string]interface{}
|
|
if len(body) > 0 {
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "Invalid JSON"})
|
|
return
|
|
}
|
|
} else {
|
|
payload = map[string]interface{}{}
|
|
}
|
|
|
|
data, _ := payload["data"].(map[string]interface{})
|
|
metaTop, _ := payload["metadata"].(map[string]interface{})
|
|
metaOrder, _ := data["orderMetadata"].(map[string]interface{})
|
|
metaLegacy, _ := data["metadata"].(map[string]interface{})
|
|
|
|
// Official: orderMerchantExternalId is our ORD-*; orderId is Waffo ORD_*. // Logan.Li
|
|
merchantExternalID := firstNonEmpty(
|
|
asString(data["orderMerchantExternalId"]),
|
|
asString(payload["orderMerchantExternalId"]),
|
|
asString(metaOrder["orderNo"]),
|
|
asString(metaOrder["order_no"]),
|
|
asString(metaLegacy["orderNo"]),
|
|
asString(metaLegacy["order_no"]),
|
|
asString(metaTop["orderNo"]),
|
|
asString(metaTop["order_no"]),
|
|
asString(payload["order_no"]),
|
|
asString(payload["orderNo"]),
|
|
c.Query("order_no"),
|
|
c.Query("orderNo"),
|
|
)
|
|
gatewayOrderNo := firstNonEmpty(
|
|
asString(data["orderId"]),
|
|
asString(payload["orderId"]),
|
|
asString(payload["gateway_order_no"]),
|
|
asString(payload["gatewayOrderNo"]),
|
|
asString(payload["id"]),
|
|
)
|
|
paymentID := firstNonEmpty(asString(data["paymentId"]), asString(payload["paymentId"]))
|
|
buyerEmail := firstNonEmpty(asString(data["buyerEmail"]), asString(payload["buyerEmail"]), asString(payload["customer_email"]))
|
|
|
|
eventType := firstNonEmpty(
|
|
c.GetHeader("X-Waffo-Event"),
|
|
asString(payload["eventType"]),
|
|
asString(payload["event_type"]),
|
|
asString(payload["type"]),
|
|
asString(payload["event"]),
|
|
"payment",
|
|
)
|
|
statusRaw := strings.ToLower(firstNonEmpty(
|
|
asString(data["orderStatus"]),
|
|
asString(data["paymentStatus"]),
|
|
asString(payload["status"]),
|
|
asString(payload["payment_status"]),
|
|
asString(payload["paymentStatus"]),
|
|
))
|
|
|
|
mappedStatus := "paid"
|
|
switch {
|
|
case strings.Contains(strings.ToLower(eventType), "refund"), strings.Contains(statusRaw, "refund"):
|
|
mappedStatus = "refunded"
|
|
case strings.Contains(strings.ToLower(eventType), "fail"), strings.Contains(strings.ToLower(eventType), "cancel"),
|
|
strings.Contains(statusRaw, "fail"), strings.Contains(statusRaw, "cancel"):
|
|
mappedStatus = "failed"
|
|
case strings.Contains(strings.ToLower(eventType), "order.completed"),
|
|
strings.Contains(strings.ToLower(eventType), "payment.succeeded"),
|
|
statusRaw == "" || strings.Contains(statusRaw, "paid") || strings.Contains(statusRaw, "complete") ||
|
|
strings.Contains(statusRaw, "success") || strings.Contains(statusRaw, "succeed"):
|
|
mappedStatus = "paid"
|
|
default:
|
|
mappedStatus = "paid"
|
|
}
|
|
|
|
// Ignore non-terminal noise if event is clearly not a completion (e.g. order.created). // Logan.Li
|
|
et := strings.ToLower(eventType)
|
|
if strings.Contains(et, "order.created") || strings.Contains(et, "order.pending") ||
|
|
strings.Contains(et, "payment.pending") || strings.Contains(et, "checkout.session") {
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ignored event"})
|
|
return
|
|
}
|
|
|
|
if merchantExternalID == "" && gatewayOrderNo == "" && buyerEmail == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "missing orderMerchantExternalId / orderId"})
|
|
return
|
|
}
|
|
|
|
req := dto.PaymentWebhookRequest{
|
|
EventType: eventType,
|
|
OrderNo: merchantExternalID,
|
|
GatewayOrderNo: firstNonEmpty(gatewayOrderNo, paymentID),
|
|
Status: mappedStatus,
|
|
CustomerEmail: buyerEmail,
|
|
Gateway: "waffo",
|
|
Payload: string(body),
|
|
Signature: sigHeader,
|
|
}
|
|
if err := ctrl.svc.PaymentWebhook(req, utils.GetClientIP(c)); err != nil {
|
|
// Still 200 so Waffo does not retry forever on business errors; log via message. // Logan.Li
|
|
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "Webhook processed"})
|
|
}
|
|
|
|
func asString(v interface{}) string {
|
|
switch t := v.(type) {
|
|
case string:
|
|
return strings.TrimSpace(t)
|
|
case float64:
|
|
if t == float64(int64(t)) {
|
|
return strconv.FormatInt(int64(t), 10)
|
|
}
|
|
return strconv.FormatFloat(t, 'f', -1, 64)
|
|
case json.Number:
|
|
return t.String()
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, v := range values {
|
|
if strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ManualGrant 手动发放 License(管理后台) // Logan.Li
|
|
func (ctrl *OrderController) ManualGrant(c *gin.Context) {
|
|
var req dto.ManualGrantRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
userID, _ := c.Get("user_id")
|
|
username, _ := c.Get("username")
|
|
data, err := ctrl.svc.ManualGrant(req, userID.(uint), username.(string), utils.GetClientIP(c))
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "License 发放成功", "data": data})
|
|
}
|
|
|
|
// CheckDuplicatePurchase 防重复购买检测(公开接口) // Logan.Li
|
|
func (ctrl *OrderController) CheckDuplicatePurchase(c *gin.Context) {
|
|
var req dto.DuplicatePurchaseCheckRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
// 通过 CheckPurchaseStatus 间接判断:有记录且有效则不允许重复购买 // Logan.Li
|
|
status, _ := ctrl.svc.CheckPurchaseStatus(req.CustomerEmail, req.ProductID)
|
|
result := dto.DuplicatePurchaseCheckResponse{AllowPurchase: true}
|
|
if status.HasActiveLicense {
|
|
result.AllowPurchase = false
|
|
result.Reason = status.Message
|
|
if result.Reason == "" {
|
|
result.Reason = "您已拥有该产品的有效授权"
|
|
}
|
|
result.ExistingLicenseKey = status.LicenseKey
|
|
result.ExistingPlanType = status.PlanType
|
|
result.CanUpgrade = true
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": result})
|
|
}
|
|
|
|
// CheckPurchaseStatus 查询购买状态(公开接口) // Logan.Li
|
|
func (ctrl *OrderController) CheckPurchaseStatus(c *gin.Context) {
|
|
var req dto.CheckPurchaseStatusRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
resp, _ := ctrl.svc.CheckPurchaseStatus(req.CustomerEmail, req.ProductID)
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": resp})
|
|
}
|