增加功能报价

This commit is contained in:
2026-09-16 00:22:17 +08:00
parent 71a14c46f9
commit 60b2658d4b
32 changed files with 3578 additions and 77 deletions
+312
View File
@@ -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)}, "获取成功")
}