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 { // 支付宝同步回跳地址携带的 out_trade_no 是支付单号(pay_no),此处兜底按支付单号查询, // 保证「支付完成自动跳回」的场景也能直接触发状态同步与自动开通。 if err2 := models.Orm.QueryTable(new(models.PlatformProductOrder)). Filter("pay_no", orderNo). Filter("tid", uint64(claims.TenantId)). Filter("delete_time__isnull", true). OrderBy("-id"). One(&order); err2 != 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)}, "获取成功") } // ============================ 套餐购买 ============================ // GetPackageList GET /backend/package/list // 租户端可购买套餐列表(仅启用的套餐) func (c *BackendProductPurchaseController) GetPackageList() { claims, err := c.claims() if err != nil { c.jsonErr(401, 401, err.Error()) return } var rows []models.SystemTenantPackage if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)). 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) } moduleMap := loadPackageModules(ids) // 该租户已购买过(已支付/已开通)的套餐:用于前端对「仅可购买一次」的体验套餐禁用购买按钮 purchased := map[uint64]bool{} if tid := uint64(claims.TenantId); tid > 0 && len(ids) > 0 { var orders []models.PlatformProductOrder if _, err := models.Orm.QueryTable(new(models.PlatformProductOrder)). Filter("tid", tid). Filter("package_id__in", ids). Filter("status__in", models.ProductOrderStatusPaid, models.ProductOrderStatusActivated). Filter("delete_time__isnull", true). All(&orders, "ID", "PackageID"); err == nil { for _, o := range orders { purchased[o.PackageID] = true } } } list := make([]map[string]interface{}, 0, len(rows)) for _, r := range rows { modules := moduleMap[r.ID] if modules == nil { modules = []tenantPackageModuleItem{} } list = append(list, map[string]interface{}{ "id": r.ID, "name": r.Name, "code": r.Code, "description": r.Description, "price": r.Price, "duration_days": r.DurationDays, "user_quota": r.UserQuota, "extra_user_price": r.ExtraUserPrice, "is_default": r.IsDefault, "allow_repurchase": r.AllowRepurchase, "purchased": purchased[r.ID], "sort": r.Sort, "status": r.Status, "remark": r.Remark, "modules": modules, "module_codes": moduleCodes(modules), "create_time": r.CreateTime, "update_time": r.UpdateTime, }) } c.jsonOK(map[string]interface{}{"list": list, "total": len(list)}, "获取成功") } // GetPackageDetailByCode GET /backend/package/detail/:code // 按套餐编码获取详情(租户端购买页使用) func (c *BackendProductPurchaseController) GetPackageDetailByCode() { 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.SystemTenantPackage if err := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("code", code). Filter("delete_time__isnull", true). Filter("status", 1). One(&row); err != nil { c.jsonErr(404, 404, "套餐不存在或已下架") return } modules := loadPackageModules([]uint64{row.ID})[row.ID] if modules == nil { modules = []tenantPackageModuleItem{} } c.jsonOK(map[string]interface{}{ "id": row.ID, "name": row.Name, "code": row.Code, "description": row.Description, "price": row.Price, "duration_days": row.DurationDays, "user_quota": row.UserQuota, "extra_user_price": row.ExtraUserPrice, "is_default": row.IsDefault, "sort": row.Sort, "status": row.Status, "remark": row.Remark, "modules": modules, "module_codes": moduleCodes(modules), "create_time": row.CreateTime, "update_time": row.UpdateTime, }, "获取成功") } type createPackageOrderPayload struct { PackageCode string `json:"package_code"` Channel string `json:"channel"` PayType string `json:"pay_type"` ReturnURL string `json:"return_url"` } // CreatePackageOrder POST /backend/package/order/create // 创建套餐购买订单;channel 非空时同时创建支付单并返回拉起支付参数。 // 订单绑定 PackageID,支付成功后自动为租户开通对应套餐。 func (c *BackendProductPurchaseController) CreatePackageOrder() { 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 createPackageOrderPayload if err := json.Unmarshal(body, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } p.PackageCode = strings.TrimSpace(p.PackageCode) p.Channel = strings.TrimSpace(p.Channel) if p.PackageCode == "" { c.jsonErr(400, 400, "套餐不能为空") return } var pkg models.SystemTenantPackage if err := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("code", p.PackageCode). Filter("delete_time__isnull", true). Filter("status", 1). One(&pkg); err != nil { c.jsonErr(404, 404, "套餐不存在或已下架") return } if pkg.Price <= 0 { c.jsonErr(400, 400, "该套餐未配置价格,请联系平台") return } // 体验套餐(allow_repurchase=0):每个租户仅可购买一次 if pkg.AllowRepurchase == 0 { cnt, _ := models.Orm.QueryTable(new(models.PlatformProductOrder)). Filter("tid", tid). Filter("package_id", pkg.ID). Filter("status__in", models.ProductOrderStatusPaid, models.ProductOrderStatusActivated). Filter("delete_time__isnull", true). Count() if cnt > 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: 0, // 套餐购买不关联产品 ProductCode: pkg.Code, ProductName: pkg.Name, Amount: pkg.Price, PackageID: pkg.ID, 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 := pkg.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, }, "下单成功") }