增加功能报价
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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)}, "获取成功")
|
||||
}
|
||||
Reference in New Issue
Block a user