Files
yunzerwebsiteallinone/go/controllers/backend_product_purchase.go
T
2026-09-16 00:22:17 +08:00

313 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"context"
"encoding/json"
"errors"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"server/services/payment"
beego "github.com/beego/beego/v2/server/web"
)
// BackendProductPurchaseController 租户端「产品购买」接口(只读产品 + 下单 + 订单状态)。
// 注意:与 CMS 产品管理的 BackendProductController 区分开。
// 租户身份来自 JWT:UserType=backend,TenantId 即租户ID。
type BackendProductPurchaseController struct {
beego.Controller
}
func (c *BackendProductPurchaseController) claims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, errors.New("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, errors.New("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, errors.New("无效的token")
}
if claims.UserType != "backend" {
return nil, errors.New("无权访问")
}
return claims, nil
}
func (c *BackendProductPurchaseController) 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 *BackendProductPurchaseController) jsonOK(data interface{}, msg string) {
if msg == "" {
msg = "success"
}
resp := map[string]interface{}{"code": 200, "msg": msg}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
func (c *BackendProductPurchaseController) tenantName(tid uint64) string {
if tid == 0 {
return ""
}
tenant := &models.SystemTenant{}
if err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("id", tid).
Filter("delete_time__isnull", true).
One(tenant); err != nil {
return ""
}
return tenant.TenantName
}
// GetProductList GET /backend/product/list
// 上架产品列表(含功能节点与启用档位),供购买页/产品列表展示
func (c *BackendProductPurchaseController) GetProductList() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var rows []models.PlatformProduct
if _, err := models.Orm.QueryTable(new(models.PlatformProduct)).
Filter("delete_time__isnull", true).
Filter("status", 1).
OrderBy("sort", "id").
All(&rows); err != nil {
c.jsonErr(500, 500, "获取失败:"+err.Error())
return
}
ids := make([]uint64, 0, len(rows))
for _, r := range rows {
ids = append(ids, r.ID)
}
featureMap := loadProductFeatures(ids)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, productToMap(r, featureMap[r.ID]))
}
c.jsonOK(map[string]interface{}{"list": list, "total": len(list)}, "获取成功")
}
// GetProductDetailByCode GET /backend/product/detail/:code
// 按产品编码取详情(租户端 home 卡片按模块 code 跳转购买页时使用)
func (c *BackendProductPurchaseController) GetProductDetailByCode() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
code := strings.TrimSpace(c.Ctx.Input.Param(":code"))
if code == "" {
c.jsonErr(400, 400, "产品编码不能为空")
return
}
var row models.PlatformProduct
if err := models.Orm.QueryTable(new(models.PlatformProduct)).
Filter("code", code).
Filter("delete_time__isnull", true).
Filter("status", 1).
One(&row); err != nil {
c.jsonErr(404, 404, "产品不存在或未上架")
return
}
c.jsonOK(productToMap(row, loadProductFeatures([]uint64{row.ID})[row.ID]), "获取成功")
}
type createProductOrderPayload struct {
ProductCode string `json:"product_code"`
Channel string `json:"channel"`
PayType string `json:"pay_type"`
ReturnURL string `json:"return_url"`
}
// CreateOrder POST /backend/product/order/create
// 创建购买订单;channel 非空时同时创建支付单并返回拉起支付参数。
func (c *BackendProductPurchaseController) CreateOrder() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
tid := uint64(claims.TenantId)
if tid == 0 {
c.jsonErr(401, 401, "未获取到租户信息")
return
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p createProductOrderPayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
p.ProductCode = strings.TrimSpace(p.ProductCode)
p.Channel = strings.TrimSpace(p.Channel)
if p.ProductCode == "" {
c.jsonErr(400, 400, "产品不能为空")
return
}
var product models.PlatformProduct
if err := models.Orm.QueryTable(new(models.PlatformProduct)).
Filter("code", p.ProductCode).
Filter("delete_time__isnull", true).
Filter("status", 1).
One(&product); err != nil {
c.jsonErr(404, 404, "产品不存在或未上架")
return
}
if product.Price <= 0 {
c.jsonErr(400, 400, "该产品未配置价格,请联系平台")
return
}
now := time.Now()
order := &models.PlatformProductOrder{
OrderNo: "PO" + strconv.FormatInt(now.UnixNano(), 10),
Tid: tid,
TenantName: c.tenantName(tid),
ProductID: product.ID,
ProductCode: product.Code,
ProductName: product.Name,
Amount: product.Price,
Status: models.ProductOrderStatusPending,
UpdateTime: &now,
}
// 未选择支付渠道:仅落单,等待平台确认(线下转账/试用审批场景)
if p.Channel == "" {
if _, err := models.Orm.Insert(order); err != nil {
c.jsonErr(500, 500, "下单失败:"+err.Error())
return
}
c.jsonOK(map[string]interface{}{
"order_no": order.OrderNo,
"pay_no": "",
"amount": order.Amount,
"offline": true,
}, "下单成功,请等待平台开通")
return
}
amountFen := int64(order.Amount*100 + 0.5)
subject := product.Name
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
payOrder, params, err := payment.CreatePayment(ctx, payment.CreateInput{
OutTradeNo: order.OrderNo,
OrderType: models.CommissionOrderTypeModuleShop,
TenantID: strconv.FormatUint(tid, 10),
TenantName: order.TenantName,
Amount: amountFen,
Channel: p.Channel,
Subject: subject,
ReturnURL: strings.TrimSpace(p.ReturnURL),
ClientIP: c.Ctx.Input.IP(),
PayType: p.PayType,
})
if err != nil {
c.jsonErr(400, 400, "创建支付单失败:"+err.Error())
return
}
order.PayNo = payOrder.PayNo
order.Channel = p.Channel
if _, err := models.Orm.Insert(order); err != nil {
c.jsonErr(500, 500, "下单失败:"+err.Error())
return
}
c.jsonOK(map[string]interface{}{
"order_no": order.OrderNo,
"pay_no": payOrder.PayNo,
"amount": order.Amount,
"channel": p.Channel,
"pay_params": params,
"offline": false,
}, "下单成功")
}
// GetOrderStatus GET /backend/product/order/status?order_no=xxx
// 查询订单状态;在线支付时同步渠道状态,已支付则自动开通套餐。
func (c *BackendProductPurchaseController) GetOrderStatus() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
orderNo := strings.TrimSpace(c.GetString("order_no"))
if orderNo == "" {
c.jsonErr(400, 400, "订单号不能为空")
return
}
var order models.PlatformProductOrder
if err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
Filter("order_no", orderNo).
Filter("tid", uint64(claims.TenantId)).
Filter("delete_time__isnull", true).
One(&order); err != nil {
c.jsonErr(404, 404, "订单不存在")
return
}
// 在线支付:同步渠道状态,支付成功后自动开通
if order.Status == models.ProductOrderStatusPending && order.PayNo != "" {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if payOrder, qerr := payment.QueryPayment(ctx, order.PayNo, true); qerr == nil && payOrder.Status == models.PayStatusPaid {
now := time.Now()
order.Status = models.ProductOrderStatusPaid
order.UpdateTime = &now
_, _ = models.Orm.Update(&order, "Status", "UpdateTime")
}
}
if order.Status == models.ProductOrderStatusPaid {
if err := activateProductOrder(&order, "支付成功自动开通"); err == nil {
_ = models.Orm.Read(&order)
}
}
c.jsonOK(map[string]interface{}{
"order_no": order.OrderNo,
"status": order.Status,
"amount": order.Amount,
"product_name": order.ProductName,
"pay_no": order.PayNo,
"activate_time": order.ActivateTime,
}, "获取成功")
}
// GetMyOrders GET /backend/product/order/list 本租户购买记录
func (c *BackendProductPurchaseController) GetMyOrders() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var rows []models.PlatformProductOrder
if _, err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
Filter("tid", uint64(claims.TenantId)).
Filter("delete_time__isnull", true).
OrderBy("-id").
Limit(50).
All(&rows); err != nil {
c.jsonErr(500, 500, "获取失败:"+err.Error())
return
}
c.jsonOK(map[string]interface{}{"list": rows, "total": len(rows)}, "获取成功")
}