完善购买和套餐功能
This commit is contained in:
Vendored
+3
@@ -18,6 +18,7 @@ declare module 'vue' {
|
|||||||
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
|
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||||
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
@@ -28,6 +29,7 @@ declare module 'vue' {
|
|||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
||||||
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
@@ -52,6 +54,7 @@ declare module 'vue' {
|
|||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
|
|||||||
@@ -231,6 +231,22 @@ export async function loadAndAddDynamicRoutes() {
|
|||||||
return routesLoadingPromise;
|
return routesLoadingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新拉取菜单并重建动态路由。
|
||||||
|
* 动态路由在构建时会把「未开通模块」的菜单注册为锁定占位页(views/locked),
|
||||||
|
* 仅调用 menuStore.refreshMenus() 只更新菜单数据、不会重建路由,
|
||||||
|
* 因此支付开通/套餐变更后必须调用本方法,才能让占位页切换为真实页面。
|
||||||
|
*/
|
||||||
|
export async function reloadMenusAndDynamicRoutes() {
|
||||||
|
const { useMenuStore } = await import("@/stores/menu");
|
||||||
|
const menuStore = useMenuStore();
|
||||||
|
// 强制从接口拉取最新菜单(含 locked 标记),写入 store 与缓存
|
||||||
|
await menuStore.refreshMenus();
|
||||||
|
// 重置加载标志后重建路由表
|
||||||
|
resetDynamicRoutes();
|
||||||
|
await loadAndAddDynamicRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
// 核心修改:移除扁平化,直接使用嵌套菜单生成路由
|
// 核心修改:移除扁平化,直接使用嵌套菜单生成路由
|
||||||
function addDynamicRoutes(menus) {
|
function addDynamicRoutes(menus) {
|
||||||
if (!menus?.length) {
|
if (!menus?.length) {
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ import {
|
|||||||
import { getTenantList } from "@/api/modules";
|
import { getTenantList } from "@/api/modules";
|
||||||
import { getProductOrderStatus } from "@/api/product";
|
import { getProductOrderStatus } from "@/api/product";
|
||||||
import { getCurrentUserProfile, logout } from "@/api/login";
|
import { getCurrentUserProfile, logout } from "@/api/login";
|
||||||
|
import { reloadMenusAndDynamicRoutes } from "@/router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useMenuStore } from "@/stores/menu";
|
import { useMenuStore } from "@/stores/menu";
|
||||||
|
|
||||||
@@ -372,7 +373,8 @@ function handleBuyProduct(module: ModuleItem) {
|
|||||||
async function handleRefreshMenus() {
|
async function handleRefreshMenus() {
|
||||||
refreshLoading.value = true;
|
refreshLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await menuStore.refreshMenus();
|
// 仅刷新菜单数据不会替换动态路由里的锁定占位页,需一并重建路由
|
||||||
|
await Promise.all([reloadMenusAndDynamicRoutes(), loadModules()]);
|
||||||
ElMessage.success("菜单已刷新");
|
ElMessage.success("菜单已刷新");
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error("刷新菜单失败");
|
ElMessage.error("刷新菜单失败");
|
||||||
@@ -462,8 +464,12 @@ async function syncReturnedPayment() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === "activated") {
|
if (status === "activated") {
|
||||||
// 订单已开通:重新拉取模块与菜单,解锁「请购买后使用」的卡片
|
// 订单已开通:重建动态路由 + 刷新模块,解锁「功能未开通」菜单占位页与「请购买后使用」卡片
|
||||||
await Promise.all([loadModules(), menuStore.refreshMenus()]);
|
try {
|
||||||
|
await Promise.all([loadModules(), reloadMenusAndDynamicRoutes()]);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("支付成功后刷新菜单失败:", e);
|
||||||
|
}
|
||||||
ElMessage.success("支付成功,功能已开通");
|
ElMessage.success("支付成功,功能已开通");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,10 +79,11 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
:loading="buyingIds[pkg.id]"
|
:loading="buyingIds[pkg.id]"
|
||||||
|
:disabled="isPurchasedOnce(pkg)"
|
||||||
@click="handleBuy(pkg)"
|
@click="handleBuy(pkg)"
|
||||||
class="buy-btn"
|
class="buy-btn"
|
||||||
>
|
>
|
||||||
立即购买
|
{{ isPurchasedOnce(pkg) ? "已购买(限购一次)" : "立即购买" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -141,6 +142,7 @@ import { ElMessage } from "element-plus";
|
|||||||
import { ArrowLeft, OfficeBuilding, Box, User, InfoFilled } from "@element-plus/icons-vue";
|
import { ArrowLeft, OfficeBuilding, Box, User, InfoFilled } from "@element-plus/icons-vue";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { reloadMenusAndDynamicRoutes } from "@/router";
|
||||||
import {
|
import {
|
||||||
getPackageList,
|
getPackageList,
|
||||||
createPackageOrder,
|
createPackageOrder,
|
||||||
@@ -223,6 +225,10 @@ const renderQR = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 仅可购买一次的套餐(体验套餐)在已购买过之后禁用购买按钮 */
|
||||||
|
const isPurchasedOnce = (pkg: any) =>
|
||||||
|
Number(pkg?.allow_repurchase) === 0 && pkg?.purchased === true;
|
||||||
|
|
||||||
const handleBuy = async (pkg: any) => {
|
const handleBuy = async (pkg: any) => {
|
||||||
buyingIds.value[pkg.id] = true;
|
buyingIds.value[pkg.id] = true;
|
||||||
try {
|
try {
|
||||||
@@ -295,6 +301,8 @@ const checkOrder = async (orderNo: string, silent = false) => {
|
|||||||
ElMessage.success("购买成功,套餐已开通");
|
ElMessage.success("购买成功,套餐已开通");
|
||||||
payVisible.value = false;
|
payVisible.value = false;
|
||||||
loadMyOrders();
|
loadMyOrders();
|
||||||
|
// 重建动态路由,把新开通模块的「功能未开通」菜单占位页切换为真实页面
|
||||||
|
reloadMenusAndDynamicRoutes().catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === "paid") {
|
if (status === "paid") {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ import {
|
|||||||
getPaymentChannels,
|
getPaymentChannels,
|
||||||
} from "@/api/product";
|
} from "@/api/product";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { reloadMenusAndDynamicRoutes } from "@/router";
|
||||||
|
|
||||||
interface FeatureNode {
|
interface FeatureNode {
|
||||||
key: number;
|
key: number;
|
||||||
@@ -311,6 +312,8 @@ const checkOrder = async (silent = false) => {
|
|||||||
ElMessage.success("购买成功,功能已开通");
|
ElMessage.success("购买成功,功能已开通");
|
||||||
payVisible.value = false;
|
payVisible.value = false;
|
||||||
loadMyOrders();
|
loadMyOrders();
|
||||||
|
// 重建动态路由,把刚开通模块的「功能未开通」菜单占位页切换为真实页面
|
||||||
|
reloadMenusAndDynamicRoutes().catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === "paid") {
|
if (status === "paid") {
|
||||||
|
|||||||
@@ -325,7 +325,8 @@ func (c *BackendProductPurchaseController) GetMyOrders() {
|
|||||||
// GetPackageList GET /backend/package/list
|
// GetPackageList GET /backend/package/list
|
||||||
// 租户端可购买套餐列表(仅启用的套餐)
|
// 租户端可购买套餐列表(仅启用的套餐)
|
||||||
func (c *BackendProductPurchaseController) GetPackageList() {
|
func (c *BackendProductPurchaseController) GetPackageList() {
|
||||||
if _, err := c.claims(); err != nil {
|
claims, err := c.claims()
|
||||||
|
if err != nil {
|
||||||
c.jsonErr(401, 401, err.Error())
|
c.jsonErr(401, 401, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -345,6 +346,22 @@ func (c *BackendProductPurchaseController) GetPackageList() {
|
|||||||
}
|
}
|
||||||
moduleMap := loadPackageModules(ids)
|
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))
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
modules := moduleMap[r.ID]
|
modules := moduleMap[r.ID]
|
||||||
@@ -361,6 +378,8 @@ func (c *BackendProductPurchaseController) GetPackageList() {
|
|||||||
"user_quota": r.UserQuota,
|
"user_quota": r.UserQuota,
|
||||||
"extra_user_price": r.ExtraUserPrice,
|
"extra_user_price": r.ExtraUserPrice,
|
||||||
"is_default": r.IsDefault,
|
"is_default": r.IsDefault,
|
||||||
|
"allow_repurchase": r.AllowRepurchase,
|
||||||
|
"purchased": purchased[r.ID],
|
||||||
"sort": r.Sort,
|
"sort": r.Sort,
|
||||||
"status": r.Status,
|
"status": r.Status,
|
||||||
"remark": r.Remark,
|
"remark": r.Remark,
|
||||||
@@ -466,6 +485,19 @@ func (c *BackendProductPurchaseController) CreatePackageOrder() {
|
|||||||
c.jsonErr(400, 400, "该套餐未配置价格,请联系平台")
|
c.jsonErr(400, 400, "该套餐未配置价格,请联系平台")
|
||||||
return
|
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()
|
now := time.Now()
|
||||||
order := &models.PlatformProductOrder{
|
order := &models.PlatformProductOrder{
|
||||||
|
|||||||
@@ -403,8 +403,8 @@ func (c *PlatformProductController) ActivateProductOrder() {
|
|||||||
c.jsonErr(404, 404, "订单不存在")
|
c.jsonErr(404, 404, "订单不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if order.Status == models.ProductOrderStatusActivated {
|
if order.Status == models.ProductOrderStatusActivated && !services.ProductOrderExpired(&order, time.Now()) {
|
||||||
c.jsonErr(400, 400, "订单已开通")
|
c.jsonErr(400, 400, "订单已开通且在有效期内")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := activateProductOrder(&order, "平台手动开通"); err != nil {
|
if err := activateProductOrder(&order, "平台手动开通"); err != nil {
|
||||||
@@ -414,18 +414,73 @@ func (c *PlatformProductController) ActivateProductOrder() {
|
|||||||
c.jsonOK(nil, "开通成功")
|
c.jsonOK(nil, "开通成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
// activateProductOrder 订单开通:绑定套餐(产品配置了套餐时)并置为已开通
|
// CloseProductOrder POST /platform/product/order/close/:id
|
||||||
|
// 关闭已开通的单品功能:订单置为已关闭,租户端对应模块权益即时回收(菜单/卡片恢复锁定)。
|
||||||
|
// 仅允许关闭单品订单(package_id=0);误操作可在购买订单列表重新「手动开通」。
|
||||||
|
func (c *PlatformProductController) CloseProductOrder() {
|
||||||
|
id := parseUint64Param(c.Ctx.Input.Param(":id"))
|
||||||
|
if id == 0 {
|
||||||
|
c.jsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var order models.PlatformProductOrder
|
||||||
|
if err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&order); err != nil {
|
||||||
|
c.jsonErr(404, 404, "订单不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if order.PackageID > 0 {
|
||||||
|
c.jsonErr(400, 400, "套餐订单请通过「套餐与用户数」调整套餐")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if order.Status != models.ProductOrderStatusActivated {
|
||||||
|
c.jsonErr(400, 400, "仅已开通的订单可关闭功能")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
order.Status = models.ProductOrderStatusClosed
|
||||||
|
order.UpdateTime = &now
|
||||||
|
order.Remark = strings.TrimSpace(order.Remark + " 平台关闭功能")
|
||||||
|
if _, err := models.Orm.Update(&order, "Status", "Remark", "UpdateTime"); err != nil {
|
||||||
|
c.jsonErr(500, 500, "关闭失败:"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.jsonOK(nil, "功能已关闭")
|
||||||
|
}
|
||||||
|
|
||||||
|
// activateProductOrder 订单开通:绑定套餐(产品配置了套餐时)并置为已开通。
|
||||||
|
// 单品功能默认 1 年有效期(ProductOrderDurationDays),到期后租户端权益自动失效,需续费。
|
||||||
func activateProductOrder(order *models.PlatformProductOrder, operator string) error {
|
func activateProductOrder(order *models.PlatformProductOrder, operator string) error {
|
||||||
|
now := time.Now()
|
||||||
if order.PackageID > 0 && order.Tid > 0 {
|
if order.PackageID > 0 && order.Tid > 0 {
|
||||||
|
// 套餐订单:绑定/续费套餐,有效期由套餐自身到期时间管理
|
||||||
if _, err := services.ApplyPackageToTenant(order.Tid, order.PackageID, true); err != nil {
|
if _, err := services.ApplyPackageToTenant(order.Tid, order.PackageID, true); err != nil {
|
||||||
return fmt.Errorf("开通失败:%v", err)
|
return fmt.Errorf("开通失败:%v", err)
|
||||||
}
|
}
|
||||||
|
} else if order.ProductID > 0 {
|
||||||
|
// 单品功能:有效期精确到日(自然日 0 点),到期日当天 0 点起即失效;
|
||||||
|
// 未到期续费从原到期日顺延 1 年,已过期重新开通从开通当日 0 点起算 1 年
|
||||||
|
base := services.DayStart(now)
|
||||||
|
if cur := services.ProductOrderExpireAt(order); cur != nil && cur.After(base) {
|
||||||
|
base = *cur
|
||||||
|
}
|
||||||
|
expire := base.AddDate(0, 0, models.ProductOrderDurationDays)
|
||||||
|
order.ExpireTime = &expire
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
order.Status = models.ProductOrderStatusActivated
|
order.Status = models.ProductOrderStatusActivated
|
||||||
order.ActivateTime = &now
|
if order.ActivateTime == nil {
|
||||||
|
// 开通时间精确到日(自然日 0 点),与有效期的计算粒度保持一致
|
||||||
|
day := services.DayStart(now)
|
||||||
|
order.ActivateTime = &day
|
||||||
|
}
|
||||||
order.UpdateTime = &now
|
order.UpdateTime = &now
|
||||||
order.Remark = strings.TrimSpace(order.Remark + " " + operator)
|
order.Remark = strings.TrimSpace(order.Remark + " " + operator)
|
||||||
_, err := models.Orm.Update(order, "Status", "ActivateTime", "Remark", "UpdateTime")
|
fields := []string{"Status", "ActivateTime", "Remark", "UpdateTime"}
|
||||||
|
if order.ExpireTime != nil {
|
||||||
|
fields = append(fields, "ExpireTime")
|
||||||
|
}
|
||||||
|
_, err := models.Orm.Update(order, fields...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,10 +103,12 @@ type tenantPackagePayload struct {
|
|||||||
UserQuota *int `json:"user_quota"`
|
UserQuota *int `json:"user_quota"`
|
||||||
ExtraUserPrice *float64 `json:"extra_user_price"`
|
ExtraUserPrice *float64 `json:"extra_user_price"`
|
||||||
IsDefault *int8 `json:"is_default"`
|
IsDefault *int8 `json:"is_default"`
|
||||||
Sort *int `json:"sort"`
|
// AllowRepurchase 是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)
|
||||||
Status *int8 `json:"status"`
|
AllowRepurchase *int8 `json:"allow_repurchase"`
|
||||||
Remark string `json:"remark"`
|
Sort *int `json:"sort"`
|
||||||
Modules []string `json:"modules"`
|
Status *int8 `json:"status"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
Modules []string `json:"modules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizePackageDuration 套餐有效时长(天)规范化:<=0 时按 365(年付)
|
// normalizePackageDuration 套餐有效时长(天)规范化:<=0 时按 365(年付)
|
||||||
@@ -193,6 +195,7 @@ func (c *PlatformTenantPackageController) GetPackageList() {
|
|||||||
"user_quota": r.UserQuota,
|
"user_quota": r.UserQuota,
|
||||||
"extra_user_price": r.ExtraUserPrice,
|
"extra_user_price": r.ExtraUserPrice,
|
||||||
"is_default": r.IsDefault,
|
"is_default": r.IsDefault,
|
||||||
|
"allow_repurchase": r.AllowRepurchase,
|
||||||
"sort": r.Sort,
|
"sort": r.Sort,
|
||||||
"status": r.Status,
|
"status": r.Status,
|
||||||
"remark": r.Remark,
|
"remark": r.Remark,
|
||||||
@@ -249,6 +252,7 @@ func (c *PlatformTenantPackageController) GetPackageSelectList() {
|
|||||||
"user_quota": r.UserQuota,
|
"user_quota": r.UserQuota,
|
||||||
"extra_user_price": r.ExtraUserPrice,
|
"extra_user_price": r.ExtraUserPrice,
|
||||||
"is_default": r.IsDefault,
|
"is_default": r.IsDefault,
|
||||||
|
"allow_repurchase": r.AllowRepurchase,
|
||||||
"modules": modules,
|
"modules": modules,
|
||||||
"module_codes": moduleCodes(modules),
|
"module_codes": moduleCodes(modules),
|
||||||
})
|
})
|
||||||
@@ -393,22 +397,27 @@ func (c *PlatformTenantPackageController) CreatePackage() {
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
row := &models.SystemTenantPackage{
|
row := &models.SystemTenantPackage{
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Code: p.Code,
|
Code: p.Code,
|
||||||
Description: strings.TrimSpace(p.Description),
|
Description: strings.TrimSpace(p.Description),
|
||||||
Price: valueFloat(p.Price, 0),
|
Price: valueFloat(p.Price, 0),
|
||||||
DurationDays: normalizePackageDuration(p.DurationDays),
|
DurationDays: normalizePackageDuration(p.DurationDays),
|
||||||
UserQuota: valueInt(p.UserQuota, services.DefaultTenantUserQuota),
|
UserQuota: valueInt(p.UserQuota, services.DefaultTenantUserQuota),
|
||||||
ExtraUserPrice: valueFloat(p.ExtraUserPrice, services.DefaultTenantExtraUserPrice),
|
ExtraUserPrice: valueFloat(p.ExtraUserPrice, services.DefaultTenantExtraUserPrice),
|
||||||
IsDefault: valueInt8(p.IsDefault, 0),
|
IsDefault: valueInt8(p.IsDefault, 0),
|
||||||
Sort: valueInt(p.Sort, 0),
|
AllowRepurchase: valueInt8(p.AllowRepurchase, 1),
|
||||||
Status: valueInt8(p.Status, 1),
|
Sort: valueInt(p.Sort, 0),
|
||||||
Remark: strings.TrimSpace(p.Remark),
|
Status: valueInt8(p.Status, 1),
|
||||||
UpdateTime: &now,
|
Remark: strings.TrimSpace(p.Remark),
|
||||||
|
UpdateTime: &now,
|
||||||
}
|
}
|
||||||
if row.UserQuota <= 0 {
|
if row.UserQuota <= 0 {
|
||||||
row.UserQuota = services.DefaultTenantUserQuota
|
row.UserQuota = services.DefaultTenantUserQuota
|
||||||
}
|
}
|
||||||
|
// 体验套餐(不允许重复购买)不涉及用户数增购,增购价恒为 0
|
||||||
|
if row.AllowRepurchase == 0 {
|
||||||
|
row.ExtraUserPrice = 0
|
||||||
|
}
|
||||||
id, err := models.Orm.Insert(row)
|
id, err := models.Orm.Insert(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.jsonErr(500, 500, "创建失败:"+err.Error())
|
c.jsonErr(500, 500, "创建失败:"+err.Error())
|
||||||
@@ -483,6 +492,26 @@ func (c *PlatformTenantPackageController) EditPackage() {
|
|||||||
update["is_default"] = *p.IsDefault
|
update["is_default"] = *p.IsDefault
|
||||||
update["status"] = 1
|
update["status"] = 1
|
||||||
}
|
}
|
||||||
|
if p.AllowRepurchase != nil {
|
||||||
|
update["allow_repurchase"] = *p.AllowRepurchase
|
||||||
|
}
|
||||||
|
|
||||||
|
// 体验套餐(不允许重复购买)不涉及用户数增购,增购价恒为 0
|
||||||
|
allowRepurchase := int8(1)
|
||||||
|
if p.AllowRepurchase != nil {
|
||||||
|
allowRepurchase = *p.AllowRepurchase
|
||||||
|
} else {
|
||||||
|
var cur models.SystemTenantPackage
|
||||||
|
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&cur); err == nil {
|
||||||
|
allowRepurchase = cur.AllowRepurchase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allowRepurchase == 0 {
|
||||||
|
update["extra_user_price"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -512,12 +541,21 @@ func clearOtherDefaultPackages(keepID uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeletePackage DELETE /platform/tenantPackage/delete/:id(软删)
|
// DeletePackage DELETE /platform/tenantPackage/delete/:id(软删)
|
||||||
|
// 注意:基础套餐(code=basic)为系统内置兜底套餐(套餐到期自动降级目标),任何情况下不允许删除。
|
||||||
func (c *PlatformTenantPackageController) DeletePackage() {
|
func (c *PlatformTenantPackageController) DeletePackage() {
|
||||||
id := parseUint64Param(c.Ctx.Input.Param(":id"))
|
id := parseUint64Param(c.Ctx.Input.Param(":id"))
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
c.jsonErr(400, 400, "参数错误")
|
c.jsonErr(400, 400, "参数错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var pkg models.SystemTenantPackage
|
||||||
|
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&pkg); err == nil && strings.EqualFold(strings.TrimSpace(pkg.Code), "basic") {
|
||||||
|
c.jsonErr(400, 400, "基础套餐为系统内置套餐,不允许删除")
|
||||||
|
return
|
||||||
|
}
|
||||||
// 有租户正在使用该套餐时不允许删除
|
// 有租户正在使用该套餐时不允许删除
|
||||||
used, _ := models.Orm.QueryTable(new(models.SystemTenant)).Filter("package_id", id).Count()
|
used, _ := models.Orm.QueryTable(new(models.SystemTenant)).Filter("package_id", id).Count()
|
||||||
if used > 0 {
|
if used > 0 {
|
||||||
@@ -744,23 +782,128 @@ func (c *PlatformTenantPackageController) GetTenantQuotaInfo() {
|
|||||||
moduleList = append(moduleList, tenantPackageModuleItem{ModuleCode: m.ModuleCode, ModuleName: m.ModuleName})
|
moduleList = append(moduleList, tenantPackageModuleItem{ModuleCode: m.ModuleCode, ModuleName: m.ModuleName})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已开通单品功能:单品订单不改变租户套餐,需与套餐模块并集展示,避免“买了功能看不到”
|
||||||
|
productOrders, productModules := loadTenantProductEntitlements(tid)
|
||||||
|
|
||||||
c.jsonOK(map[string]interface{}{
|
c.jsonOK(map[string]interface{}{
|
||||||
"tid": info.Tid,
|
"tid": info.Tid,
|
||||||
"package_id": info.PackageID,
|
"package_id": info.PackageID,
|
||||||
"effective_package_id": info.EffectivePackageID,
|
"effective_package_id": info.EffectivePackageID,
|
||||||
"package_name": info.PackageName,
|
"package_name": info.PackageName,
|
||||||
"modules": moduleList,
|
"modules": moduleList,
|
||||||
"quota": info.Quota,
|
"product_modules": productModules,
|
||||||
"used": info.Used,
|
"product_orders": productOrders,
|
||||||
"remaining": info.Remaining,
|
"quota": info.Quota,
|
||||||
"extra_user_price": info.ExtraUserPrice,
|
"used": info.Used,
|
||||||
"duration_days": info.DurationDays,
|
"remaining": info.Remaining,
|
||||||
"package_expire_time": info.PackageExpireTime,
|
"extra_user_price": info.ExtraUserPrice,
|
||||||
"days_remaining": info.DaysRemaining,
|
"duration_days": info.DurationDays,
|
||||||
"quota_packages": quotaPackages,
|
"package_expire_time": info.PackageExpireTime,
|
||||||
|
"days_remaining": info.DaysRemaining,
|
||||||
|
"quota_packages": quotaPackages,
|
||||||
}, "获取成功")
|
}, "获取成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// productEntitlement 已开通单品功能明细(租户详情「套餐与用户数」展示 / 关闭用)
|
||||||
|
type productEntitlement struct {
|
||||||
|
OrderID uint64 `json:"order_id"`
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
ProductID uint64 `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ModuleCode string `json:"module_code"`
|
||||||
|
ModuleName string `json:"module_name"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
ActivateTime *time.Time `json:"activate_time"`
|
||||||
|
ExpireTime *time.Time `json:"expire_time"`
|
||||||
|
// Expired 是否已过有效期:到期后租户端权益自动失效,需续费
|
||||||
|
Expired bool `json:"expired"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTenantProductEntitlements 加载租户已开通的单品功能:
|
||||||
|
// - orders 订单明细(按开通时间倒序),供面板展示与「关闭功能」操作;
|
||||||
|
// - modules 去重后的模块列表,与套餐模块并集构成租户完整功能权益。
|
||||||
|
func loadTenantProductEntitlements(tid uint64) ([]productEntitlement, []tenantPackageModuleItem) {
|
||||||
|
orders := make([]productEntitlement, 0)
|
||||||
|
modules := make([]tenantPackageModuleItem, 0)
|
||||||
|
if tid == 0 {
|
||||||
|
return orders, modules
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.PlatformProductOrder
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("status", models.ProductOrderStatusActivated).
|
||||||
|
Filter("product_id__gt", 0).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
OrderBy("-id").
|
||||||
|
All(&rows); err != nil || len(rows) == 0 {
|
||||||
|
return orders, modules
|
||||||
|
}
|
||||||
|
|
||||||
|
productIDs := make([]uint64, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
productIDs = append(productIDs, r.ProductID)
|
||||||
|
}
|
||||||
|
var products []models.PlatformProduct
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.PlatformProduct)).
|
||||||
|
Filter("id__in", productIDs).
|
||||||
|
All(&products)
|
||||||
|
productMap := make(map[uint64]models.PlatformProduct, len(products))
|
||||||
|
for _, p := range products {
|
||||||
|
productMap[p.ID] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块 code -> 名称(大小写/空格不敏感,与租户端权益判定保持一致)
|
||||||
|
var moduleRows []models.SystemModules
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
All(&moduleRows)
|
||||||
|
moduleNameMap := make(map[string]string, len(moduleRows))
|
||||||
|
for _, m := range moduleRows {
|
||||||
|
moduleNameMap[services.NormalizeModuleCode(m.Code)] = m.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, r := range rows {
|
||||||
|
code, name := "", ""
|
||||||
|
if p, ok := productMap[r.ProductID]; ok {
|
||||||
|
code = strings.TrimSpace(p.ModuleCode)
|
||||||
|
if code == "" {
|
||||||
|
// 产品未显式关联模块时按产品编码兜底(与 GetTenantModuleCodes 一致)
|
||||||
|
code = strings.TrimSpace(p.Code)
|
||||||
|
}
|
||||||
|
name = p.Name
|
||||||
|
}
|
||||||
|
normCode := services.NormalizeModuleCode(code)
|
||||||
|
expire := services.ProductOrderExpireAt(&r)
|
||||||
|
expired := services.ProductOrderExpired(&r, now)
|
||||||
|
// 权益模块只统计仍在有效期内的订单,与租户端 GetTenantModuleCodes 判定保持一致
|
||||||
|
if !expired && normCode != "" && !seen[normCode] {
|
||||||
|
seen[normCode] = true
|
||||||
|
moduleName := moduleNameMap[normCode]
|
||||||
|
if moduleName == "" {
|
||||||
|
moduleName = name
|
||||||
|
}
|
||||||
|
modules = append(modules, tenantPackageModuleItem{ModuleCode: code, ModuleName: moduleName})
|
||||||
|
}
|
||||||
|
// 明细保留全部已开通订单(含已过期):便于平台看到「已过期,待续费并关闭」
|
||||||
|
orders = append(orders, productEntitlement{
|
||||||
|
OrderID: r.ID,
|
||||||
|
OrderNo: r.OrderNo,
|
||||||
|
ProductID: r.ProductID,
|
||||||
|
ProductName: name,
|
||||||
|
ModuleCode: code,
|
||||||
|
ModuleName: moduleNameMap[normCode],
|
||||||
|
Amount: r.Amount,
|
||||||
|
ActivateTime: r.ActivateTime,
|
||||||
|
ExpireTime: expire,
|
||||||
|
Expired: expired,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return orders, modules
|
||||||
|
}
|
||||||
|
|
||||||
type rechargeQuotaPayload struct {
|
type rechargeQuotaPayload struct {
|
||||||
Tid uint64 `json:"tid"`
|
Tid uint64 `json:"tid"`
|
||||||
Type int8 `json:"type"` // 1 单个增购 2 套餐增购
|
Type int8 `json:"type"` // 1 单个增购 2 套餐增购
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ CREATE TABLE IF NOT EXISTS `yz_platform_product_order` (
|
|||||||
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '支付渠道:wechat/alipay/... 空=线下',
|
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '支付渠道:wechat/alipay/... 空=线下',
|
||||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending待支付 paid已支付 activated已开通 closed已关闭',
|
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending待支付 paid已支付 activated已开通 closed已关闭',
|
||||||
`activate_time` datetime DEFAULT NULL COMMENT '开通时间',
|
`activate_time` datetime DEFAULT NULL COMMENT '开通时间',
|
||||||
|
`expire_time` datetime DEFAULT NULL COMMENT '有效期截止时间(单品功能默认开通后 1 年,到期权益自动失效,需续费)',
|
||||||
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`update_time` datetime DEFAULT NULL,
|
`update_time` datetime DEFAULT NULL,
|
||||||
@@ -82,6 +83,12 @@ SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA
|
|||||||
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_feature` ADD COLUMN `pid` bigint(20) NOT NULL DEFAULT 0 COMMENT ''上级节点ID,0=顶级节点'' AFTER `product_id`', 'DO 0');
|
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_feature` ADD COLUMN `pid` bigint(20) NOT NULL DEFAULT 0 COMMENT ''上级节点ID,0=顶级节点'' AFTER `product_id`', 'DO 0');
|
||||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 购买订单有效期字段(历史库补齐;单品功能默认开通后 1 年有效)
|
||||||
|
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_order');
|
||||||
|
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_order' AND COLUMN_NAME = 'expire_time');
|
||||||
|
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_order` ADD COLUMN `expire_time` datetime DEFAULT NULL COMMENT ''有效期截止时间(单品功能默认开通后 1 年,到期权益自动失效,需续费)'' AFTER `activate_time`', 'DO 0');
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-- 5) 平台端菜单:基础设置 → 产品定价(仅平台端可见)
|
-- 5) 平台端菜单:基础设置 → 产品定价(仅平台端可见)
|
||||||
-- pid=17 为「基础设置」菜单;若环境中 17 不是基础设置,请在平台端「菜单管理」里手工挂载本页面:
|
-- pid=17 为「基础设置」菜单;若环境中 17 不是基础设置,请在平台端「菜单管理」里手工挂载本页面:
|
||||||
-- 组件路径 /basicSettings/pricing/index.vue,路径 /basicSettings/pricing
|
-- 组件路径 /basicSettings/pricing/index.vue,路径 /basicSettings/pricing
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS `yz_system_tenant_package` (
|
|||||||
`user_quota` int(11) NOT NULL DEFAULT 20 COMMENT '套餐包含的用户数(新租户绑定套餐后初始化的用户数上限)',
|
`user_quota` int(11) NOT NULL DEFAULT 20 COMMENT '套餐包含的用户数(新租户绑定套餐后初始化的用户数上限)',
|
||||||
`extra_user_price` decimal(10,2) NOT NULL DEFAULT 200.00 COMMENT '单个用户增购价格(元/人)',
|
`extra_user_price` decimal(10,2) NOT NULL DEFAULT 200.00 COMMENT '单个用户增购价格(元/人)',
|
||||||
`is_default` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否默认套餐:1是 0否(新建租户未指定套餐时使用)',
|
`is_default` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否默认套餐:1是 0否(新建租户未指定套餐时使用)',
|
||||||
|
`allow_repurchase` tinyint(4) NOT NULL DEFAULT 1 COMMENT '是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)',
|
||||||
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
|
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
|
||||||
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
|
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
|
||||||
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
@@ -83,6 +84,15 @@ CREATE TABLE IF NOT EXISTS `yz_system_tenant_quota_order` (
|
|||||||
KEY `idx_tid` (`tid`)
|
KEY `idx_tid` (`tid`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户用户数增购记录表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户用户数增购记录表';
|
||||||
|
|
||||||
|
-- 4.5) 历史库补列:套餐是否允许重复购买(0=仅可购买一次,适用于体验套餐)
|
||||||
|
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package');
|
||||||
|
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package' AND COLUMN_NAME = 'allow_repurchase');
|
||||||
|
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant_package` ADD COLUMN `allow_repurchase` tinyint(4) NOT NULL DEFAULT 1 COMMENT ''是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)'' AFTER `is_default`', 'DO 0');
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 体验套餐(不允许重复购买)不涉及用户数增购:清理历史增购价
|
||||||
|
UPDATE `yz_system_tenant_package` SET `extra_user_price` = 0 WHERE `allow_repurchase` = 0;
|
||||||
|
|
||||||
-- 5) 租户表补列:套餐ID + 用户数上限
|
-- 5) 租户表补列:套餐ID + 用户数上限
|
||||||
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant');
|
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant');
|
||||||
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant' AND COLUMN_NAME = 'package_id');
|
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant' AND COLUMN_NAME = 'package_id');
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ const (
|
|||||||
ProductOrderStatusClosed = "closed" // 已关闭
|
ProductOrderStatusClosed = "closed" // 已关闭
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProductOrderDurationDays 单品功能默认有效期(天):1 年。
|
||||||
|
// 到期后租户端权益自动失效(GetTenantModuleCodes 按有效期过滤),需重新购买续费。
|
||||||
|
const ProductOrderDurationDays = 365
|
||||||
|
|
||||||
// PlatformProductOrder 产品购买订单 yz_platform_product_order
|
// PlatformProductOrder 产品购买订单 yz_platform_product_order
|
||||||
type PlatformProductOrder struct {
|
type PlatformProductOrder struct {
|
||||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
@@ -60,6 +64,7 @@ type PlatformProductOrder struct {
|
|||||||
Channel string `orm:"column(channel);size(32)" json:"channel"`
|
Channel string `orm:"column(channel);size(32)" json:"channel"`
|
||||||
Status string `orm:"column(status);size(20);default(pending)" json:"status"`
|
Status string `orm:"column(status);size(20);default(pending)" json:"status"`
|
||||||
ActivateTime *time.Time `orm:"column(activate_time);type(datetime);null" json:"activate_time"`
|
ActivateTime *time.Time `orm:"column(activate_time);type(datetime);null" json:"activate_time"`
|
||||||
|
ExpireTime *time.Time `orm:"column(expire_time);type(datetime);null" json:"expire_time"`
|
||||||
Remark string `orm:"column(remark);size(255)" json:"remark"`
|
Remark string `orm:"column(remark);size(255)" json:"remark"`
|
||||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ type SystemTenantPackage struct {
|
|||||||
UserQuota int `orm:"column(user_quota);default(20)" json:"user_quota"`
|
UserQuota int `orm:"column(user_quota);default(20)" json:"user_quota"`
|
||||||
ExtraUserPrice float64 `orm:"column(extra_user_price);digits(10);decimals(2)" json:"extra_user_price"`
|
ExtraUserPrice float64 `orm:"column(extra_user_price);digits(10);decimals(2)" json:"extra_user_price"`
|
||||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
// AllowRepurchase 是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐场景)
|
||||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
AllowRepurchase int8 `orm:"column(allow_repurchase);default(1)" json:"allow_repurchase"`
|
||||||
Remark string `orm:"column(remark);size(255)" json:"remark"`
|
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||||
|
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||||
|
Remark string `orm:"column(remark);size(255)" json:"remark"`
|
||||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func Register() {
|
|||||||
beego.Router("/platform/product/delete/:id", &controllers.PlatformProductController{}, "delete:DeleteProduct")
|
beego.Router("/platform/product/delete/:id", &controllers.PlatformProductController{}, "delete:DeleteProduct")
|
||||||
beego.Router("/platform/product/orders", &controllers.PlatformProductController{}, "get:GetProductOrders")
|
beego.Router("/platform/product/orders", &controllers.PlatformProductController{}, "get:GetProductOrders")
|
||||||
beego.Router("/platform/product/order/activate/:id", &controllers.PlatformProductController{}, "post:ActivateProductOrder")
|
beego.Router("/platform/product/order/activate/:id", &controllers.PlatformProductController{}, "post:ActivateProductOrder")
|
||||||
|
beego.Router("/platform/product/order/close/:id", &controllers.PlatformProductController{}, "post:CloseProductOrder")
|
||||||
|
|
||||||
// 用户数加购规格(yz_system_tenant_user_quota_package)
|
// 用户数加购规格(yz_system_tenant_user_quota_package)
|
||||||
beego.Router("/platform/tenantQuotaPackage/list", &controllers.PlatformTenantPackageController{}, "get:GetQuotaPackageList")
|
beego.Router("/platform/tenantQuotaPackage/list", &controllers.PlatformTenantPackageController{}, "get:GetQuotaPackageList")
|
||||||
|
|||||||
@@ -151,6 +151,37 @@ func NormalizeModuleCode(code string) string {
|
|||||||
return strings.ToLower(strings.TrimSpace(code))
|
return strings.ToLower(strings.TrimSpace(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DayStart 取时间所在自然日的 0 点(有效期按「日」计算,不精确到秒)
|
||||||
|
func DayStart(t time.Time) time.Time {
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductOrderExpireAt 单品订单有效期截止时间(精确到日):
|
||||||
|
// - 优先取落库的 expire_time(按日规范化到当天 0 点);
|
||||||
|
// - 历史订单未落库时按「开通日 0 点 + 默认有效期(1 年)」兜底计算,保证老订单同样按 1 年到期;
|
||||||
|
// - 两者都无(尚未开通)返回 nil,视为无到期时间。
|
||||||
|
func ProductOrderExpireAt(order *models.PlatformProductOrder) *time.Time {
|
||||||
|
if order == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if order.ExpireTime != nil {
|
||||||
|
t := DayStart(*order.ExpireTime)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
if order.ActivateTime != nil {
|
||||||
|
t := DayStart(*order.ActivateTime).AddDate(0, 0, models.ProductOrderDurationDays)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductOrderExpired 单品订单是否已过有效期:
|
||||||
|
// 到期日当天 0 点起即视为过期(到期后权益自动失效,需重新购买续费)。
|
||||||
|
func ProductOrderExpired(order *models.PlatformProductOrder, now time.Time) bool {
|
||||||
|
expire := ProductOrderExpireAt(order)
|
||||||
|
return expire != nil && !now.Before(*expire)
|
||||||
|
}
|
||||||
|
|
||||||
// GetTenantModuleCodes 获取租户已开通的功能模块编码集合(如 erp / oa / crm)。
|
// GetTenantModuleCodes 获取租户已开通的功能模块编码集合(如 erp / oa / crm)。
|
||||||
//
|
//
|
||||||
// 权益由两部分组成:
|
// 权益由两部分组成:
|
||||||
@@ -189,8 +220,21 @@ func GetTenantModuleCodes(tid uint64) map[string]bool {
|
|||||||
return codes
|
return codes
|
||||||
}
|
}
|
||||||
|
|
||||||
productIDs := make([]uint64, 0, len(orders))
|
// 只统计仍在有效期内的订单:到期后权益自动失效,需重新购买续费
|
||||||
|
now := time.Now()
|
||||||
|
validOrders := make([]models.PlatformProductOrder, 0, len(orders))
|
||||||
for _, order := range orders {
|
for _, order := range orders {
|
||||||
|
if ProductOrderExpired(&order, now) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validOrders = append(validOrders, order)
|
||||||
|
}
|
||||||
|
if len(validOrders) == 0 {
|
||||||
|
return codes
|
||||||
|
}
|
||||||
|
|
||||||
|
productIDs := make([]uint64, 0, len(validOrders))
|
||||||
|
for _, order := range validOrders {
|
||||||
productIDs = append(productIDs, order.ProductID)
|
productIDs = append(productIDs, order.ProductID)
|
||||||
}
|
}
|
||||||
var products []models.PlatformProduct
|
var products []models.PlatformProduct
|
||||||
|
|||||||
@@ -63,3 +63,11 @@ export function activateProductOrder(id) {
|
|||||||
method: "post",
|
method: "post",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 关闭已开通的单品功能(回收模块权益,可在购买订单中重新开通) */
|
||||||
|
export function closeProductOrder(id) {
|
||||||
|
return request({
|
||||||
|
url: `/platform/product/order/close/${id}`,
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,9 +57,16 @@
|
|||||||
:min="0"
|
:min="0"
|
||||||
:max="999999"
|
:max="999999"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
|
:disabled="Number(form.allow_repurchase) === 0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
/>
|
/>
|
||||||
<span class="field-tip">元/人,单个增购用户数时的默认单价(如 200)</span>
|
<span class="field-tip">
|
||||||
|
{{
|
||||||
|
Number(form.allow_repurchase) === 0
|
||||||
|
? "体验套餐(仅可购买一次)不涉及用户数增购,无需设置"
|
||||||
|
: "元/人,单个增购用户数时的默认单价(如 200)"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="套餐价格" prop="price">
|
<el-form-item label="套餐价格" prop="price">
|
||||||
@@ -103,6 +110,11 @@
|
|||||||
<span class="field-tip">新建租户未指定套餐时默认使用</span>
|
<span class="field-tip">新建租户未指定套餐时默认使用</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="允许重复购买" prop="allow_repurchase">
|
||||||
|
<el-switch v-model="form.allow_repurchase" :active-value="1" :inactive-value="0" />
|
||||||
|
<span class="field-tip">关闭后每个租户仅可购买一次(体验套餐),已购买过则无法再次下单</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-radio-group v-model="form.status">
|
<el-radio-group v-model="form.status">
|
||||||
<el-radio :label="1">启用</el-radio>
|
<el-radio :label="1">启用</el-radio>
|
||||||
@@ -161,6 +173,7 @@ const emptyForm = () => ({
|
|||||||
user_quota: 20,
|
user_quota: 20,
|
||||||
extra_user_price: 200,
|
extra_user_price: 200,
|
||||||
is_default: 0,
|
is_default: 0,
|
||||||
|
allow_repurchase: 1,
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: "",
|
remark: "",
|
||||||
@@ -206,6 +219,16 @@ const handleCheckAllModules = (checked: boolean) => {
|
|||||||
form.value.modules = checked ? moduleOptions.value.map((m) => m.code) : [];
|
form.value.modules = checked ? moduleOptions.value.map((m) => m.code) : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 体验套餐(不允许重复购买)不涉及用户数增购:增购价自动清零
|
||||||
|
watch(
|
||||||
|
() => form.value.allow_repurchase,
|
||||||
|
(val) => {
|
||||||
|
if (Number(val) === 0) {
|
||||||
|
form.value.extra_user_price = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const loadModuleOptions = async () => {
|
const loadModuleOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getTenantPackageModuleOptions();
|
const res = await getTenantPackageModuleOptions();
|
||||||
@@ -232,6 +255,7 @@ const loadDetail = async (id: number) => {
|
|||||||
user_quota: Number(d.user_quota || 20),
|
user_quota: Number(d.user_quota || 20),
|
||||||
extra_user_price: Number(d.extra_user_price || 0),
|
extra_user_price: Number(d.extra_user_price || 0),
|
||||||
is_default: Number(d.is_default || 0),
|
is_default: Number(d.is_default || 0),
|
||||||
|
allow_repurchase: Number(d.allow_repurchase ?? 1),
|
||||||
sort: Number(d.sort || 0),
|
sort: Number(d.sort || 0),
|
||||||
status: Number(d.status ?? 1),
|
status: Number(d.status ?? 1),
|
||||||
remark: d.remark || "",
|
remark: d.remark || "",
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
|
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
<el-alert type="info" :closable="false" show-icon class="tip-alert">
|
<!-- <el-alert type="info" :closable="false" show-icon class="tip-alert">
|
||||||
<template #default>
|
<template #default>
|
||||||
<p>1、套餐决定租户能开通哪些功能:勾选「功能模块」后,绑定该套餐的租户端只展示对应功能菜单(如 ERP 套餐开通进销存)。</p>
|
<p>1、套餐决定租户能开通哪些功能:勾选「功能模块」后,绑定该套餐的租户端只展示对应功能菜单(如 ERP 套餐开通进销存)。</p>
|
||||||
<p>2、套餐按「有效时长」计费(默认 365 天,价格即元/年):到期后自动切换为基础套餐,续费后恢复对应功能。</p>
|
<p>2、套餐按「有效时长」计费(默认 365 天,价格即元/年):到期后自动切换为基础套餐,续费后恢复对应功能。</p>
|
||||||
<p>3、所有租户都有用户数上限(默认 20 人):达到上限后无法再开账号,可在「租户管理 → 租户详情 → 套餐与用户数」中按单个(如 200 元/人)或加购套餐(如 5 人 500 元)增购。</p>
|
<p>3、所有租户都有用户数上限(默认 20 人):达到上限后无法再开账号,可在「租户管理 → 租户详情 → 套餐与用户数」中按单个(如 200 元/人)或加购套餐(如 5 人 500 元)增购。</p>
|
||||||
</template>
|
</template>
|
||||||
</el-alert>
|
</el-alert> -->
|
||||||
|
|
||||||
<div class="purchase-url-bar">
|
<div class="purchase-url-bar">
|
||||||
<span class="bar-label">套餐购买链接</span>
|
<span class="bar-label">套餐购买链接</span>
|
||||||
@@ -88,6 +88,17 @@
|
|||||||
<el-table-column prop="tenant_count" label="使用租户" width="90" align="center">
|
<el-table-column prop="tenant_count" label="使用租户" width="90" align="center">
|
||||||
<template #default="{ row }">{{ row.tenant_count || 0 }}</template>
|
<template #default="{ row }">{{ row.tenant_count || 0 }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="重复购买" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="Number(row.allow_repurchase) === 0 ? 'warning' : 'success'"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ Number(row.allow_repurchase) === 0 ? "仅一次" : "允许" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -101,7 +112,14 @@
|
|||||||
<el-button size="small" type="primary" link @click="handleEditPackage(row)">
|
<el-button size="small" type="primary" link @click="handleEditPackage(row)">
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" type="danger" link @click="handleDeletePackage(row)">
|
<!-- 基础套餐(code=basic)为系统内置兜底套餐,不提供删除入口 -->
|
||||||
|
<el-button
|
||||||
|
v-if="!isBasicPackage(row)"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="handleDeletePackage(row)"
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -271,6 +289,10 @@ const handleEditPackage = (row: any) => {
|
|||||||
packageDialogVisible.value = true;
|
packageDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 基础套餐(code=basic)为系统内置兜底套餐(套餐到期自动降级目标),不允许删除 */
|
||||||
|
const isBasicPackage = (row: any) =>
|
||||||
|
String(row?.code || "").trim().toLowerCase() === "basic";
|
||||||
|
|
||||||
const handleDeletePackage = async (row: any) => {
|
const handleDeletePackage = async (row: any) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tenant-package-tab" v-loading="loading">
|
<div class="tenant-package-tab" v-loading="loading">
|
||||||
|
<div class="panel-toolbar">
|
||||||
|
<el-button size="small" :loading="loading" @click="refresh">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新数据
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
<el-descriptions :column="2" border class="quota-desc">
|
<el-descriptions :column="2" border class="quota-desc">
|
||||||
<el-descriptions-item label="当前套餐">
|
<el-descriptions-item label="当前套餐">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -32,13 +38,27 @@
|
|||||||
<span v-else class="muted">永久有效</span>
|
<span v-else class="muted">永久有效</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="开通功能">
|
<el-descriptions-item label="开通功能">
|
||||||
<template v-if="info.module_names && info.module_names.length">
|
<template v-if="info.modules && info.modules.length">
|
||||||
<el-tag v-for="(m, i) in info.module_names" :key="i" size="small" class="module-tag">
|
<el-tag v-for="(m, i) in info.modules" :key="i" size="small" class="module-tag">
|
||||||
{{ m }}
|
{{ m.module_name || m.module_code }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="muted">未配置功能</span>
|
<span v-else class="muted">未配置功能</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="单品功能">
|
||||||
|
<template v-if="info.product_modules && info.product_modules.length">
|
||||||
|
<el-tag
|
||||||
|
v-for="(m, i) in info.product_modules"
|
||||||
|
:key="i"
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
class="module-tag"
|
||||||
|
>
|
||||||
|
{{ m.module_name || m.module_code }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else class="muted">无单品购买</span>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="用户数使用">
|
<el-descriptions-item label="用户数使用">
|
||||||
<span class="quota-text">
|
<span class="quota-text">
|
||||||
<b :class="{ danger: info.remaining <= 0 }">{{ info.used }}</b> / {{ info.quota }} 人
|
<b :class="{ danger: info.remaining <= 0 }">{{ info.used }}</b> / {{ info.quota }} 人
|
||||||
@@ -52,7 +72,10 @@
|
|||||||
<span class="muted">剩余 {{ info.remaining }} 个名额</span>
|
<span class="muted">剩余 {{ info.remaining }} 个名额</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="增购单价">
|
<el-descriptions-item label="增购单价">
|
||||||
<span>{{ formatMoney(info.extra_user_price) }}/人</span>
|
<template v-if="Number(info.extra_user_price) > 0">
|
||||||
|
<span>{{ formatMoney(info.extra_user_price) }}/人</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="muted">未设置</span>
|
||||||
<el-button type="primary" link class="ml8" @click="openRecharge">增购用户数</el-button>
|
<el-button type="primary" link class="ml8" @click="openRecharge">增购用户数</el-button>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -74,6 +97,51 @@
|
|||||||
title="该租户套餐即将到期,到期后将自动切换为基础套餐,请及时续费"
|
title="该租户套餐即将到期,到期后将自动切换为基础套餐,请及时续费"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="section-title">已购功能(单品购买)</div>
|
||||||
|
<el-table :data="info.product_orders || []" style="width: 100%" size="small">
|
||||||
|
<el-table-column prop="product_name" label="产品" min-width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column label="功能模块" width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.module_name || row.module_code" size="small" effect="plain">
|
||||||
|
{{ row.module_name || row.module_code }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else class="muted">未关联</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="金额" width="100" align="center">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="开通时间" min-width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.activate_time ? formatDate(row.activate_time) : "-" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="过期时间" min-width="180" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row.expire_time">
|
||||||
|
<span>{{ formatDate(row.expire_time) }}</span>
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="row.expired ? 'danger' : 'success'"
|
||||||
|
effect="plain"
|
||||||
|
class="ml8"
|
||||||
|
>
|
||||||
|
{{ row.expired ? "已过期" : `剩余 ${remainingDays(row.expire_time)} 天` }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else class="muted">长期有效</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="danger" link @click="handleCloseEntitlement(row)">
|
||||||
|
关闭功能
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
<div class="section-title">增购记录</div>
|
<div class="section-title">增购记录</div>
|
||||||
<el-table :data="orders" style="width: 100%" size="small" v-loading="ordersLoading">
|
<el-table :data="orders" style="width: 100%" size="small" v-loading="ordersLoading">
|
||||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||||
@@ -171,7 +239,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { formatDateTime } from "@/utils/datetime";
|
import { Refresh } from "@element-plus/icons-vue";
|
||||||
|
import { formatDate, formatDateTime } from "@/utils/datetime";
|
||||||
import {
|
import {
|
||||||
getTenantQuotaInfo,
|
getTenantQuotaInfo,
|
||||||
getTenantQuotaOrders,
|
getTenantQuotaOrders,
|
||||||
@@ -179,6 +248,7 @@ import {
|
|||||||
setTenantPackage,
|
setTenantPackage,
|
||||||
rechargeTenantQuota,
|
rechargeTenantQuota,
|
||||||
} from "@/api/tenantPackage";
|
} from "@/api/tenantPackage";
|
||||||
|
import { closeProductOrder } from "@/api/product";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 当前租户 ID,为空时不请求 */
|
/** 当前租户 ID,为空时不请求 */
|
||||||
@@ -203,6 +273,11 @@ const rechargeForm = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatMoney = (v: any) => `¥${Number(v || 0).toFixed(2)}`;
|
const formatMoney = (v: any) => `¥${Number(v || 0).toFixed(2)}`;
|
||||||
|
/** 距到期剩余天数(向上取整,最小 0) */
|
||||||
|
const remainingDays = (t: string) => {
|
||||||
|
const ms = new Date(t).getTime() - Date.now();
|
||||||
|
return Math.max(0, Math.ceil(ms / 86400000));
|
||||||
|
};
|
||||||
/** 套餐价格单位随有效时长变化:365天=元/年,30=元/月,90=元/季,其余=元/N天 */
|
/** 套餐价格单位随有效时长变化:365天=元/年,30=元/月,90=元/季,其余=元/N天 */
|
||||||
const priceUnit = (row: any) => {
|
const priceUnit = (row: any) => {
|
||||||
const d = Number(row?.duration_days || 365);
|
const d = Number(row?.duration_days || 365);
|
||||||
@@ -336,6 +411,32 @@ const handleSetPackage = async (id: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 关闭已开通的单品功能:订单置为已关闭,租户端对应模块权益即时回收 */
|
||||||
|
const handleCloseEntitlement = async (row: any) => {
|
||||||
|
if (!row?.order_id) return;
|
||||||
|
const label = row.module_name || row.product_name || "该";
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`关闭后该租户将立即失去「${label}」功能的使用权限(订单保留,可在「产品定价 - 购买订单」中重新开通),确定继续吗?`,
|
||||||
|
"关闭功能",
|
||||||
|
{ type: "warning", confirmButtonText: "确认关闭" }
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await closeProductOrder(row.order_id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("功能已关闭");
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "关闭失败");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || "关闭失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openRecharge = () => {
|
const openRecharge = () => {
|
||||||
rechargeForm.value = {
|
rechargeForm.value = {
|
||||||
type: 1,
|
type: 1,
|
||||||
@@ -391,6 +492,11 @@ defineExpose({ refresh });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.panel-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
.quota-desc {
|
.quota-desc {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,11 +35,11 @@
|
|||||||
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
|
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
|
||||||
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
|
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
|
||||||
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
|
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
|
||||||
<el-table-column prop="org_id" label="部门" min-width="160" align="center">
|
<!-- <el-table-column prop="org_id" label="部门" min-width="160" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ orgNameMap[Number(row.org_id)] || (Number(row.org_id) ? `部门${row.org_id}` : "未分配") }}
|
{{ orgNameMap[Number(row.org_id)] || (Number(row.org_id) ? `部门${row.org_id}` : "未分配") }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
|
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
|
||||||
@@ -47,20 +47,21 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="320" align="center" fixed="right">
|
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button text type="primary" @click="openEditDialog(row)">编辑</el-button>
|
<el-button text size="small" type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||||
<el-button text type="primary" @click="openPasswordDialog(row)">
|
<el-button text size="small" type="primary" @click="openPasswordDialog(row)">
|
||||||
修改密码
|
修改密码
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
text
|
text
|
||||||
|
size="small"
|
||||||
:type="Number(row.status) === 1 ? 'warning' : 'success'"
|
:type="Number(row.status) === 1 ? 'warning' : 'success'"
|
||||||
@click="handleToggleStatus(row)"
|
@click="handleToggleStatus(row)"
|
||||||
>
|
>
|
||||||
{{ Number(row.status) === 1 ? "禁用" : "启用" }}
|
{{ Number(row.status) === 1 ? "禁用" : "启用" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button text type="danger" @click="handleDelete(row)">删除</el-button>
|
<el-button text size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|||||||
Reference in New Issue
Block a user