feat(tenant): 添加租户名称重复验证

- 在创建租户时检查名称是否已存在
- 确保租户名称不为空
- 防止重复租户名称导致的数据冲突
This commit is contained in:
2025-12-12 16:35:36 +08:00
parent d82d78697e
commit 980f8f187e
19 changed files with 1362 additions and 2 deletions

View File

@@ -0,0 +1,54 @@
package com.gxwebsoft.common.system.controller;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.TenantPackage;
import com.gxwebsoft.common.system.service.TenantPackageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 租户套餐控制器
*
* @author WebSoft
* @since 2025-12-12
*/
@Tag(name = "租户套餐管理")
@RestController
@RequestMapping("/api/system/package")
public class TenantPackageController extends BaseController {
@Resource
private TenantPackageService tenantPackageService;
@Operation(summary = "查询所有上架套餐")
@GetMapping("/list")
public ApiResult<List<TenantPackage>> list() {
List<TenantPackage> packages = tenantPackageService.listAvailablePackages();
return success(packages);
}
@Operation(summary = "根据ID查询套餐详情")
@GetMapping("/{id}")
public ApiResult<TenantPackage> getById(@PathVariable("id") Integer id) {
TenantPackage tenantPackage = tenantPackageService.getById(id);
if (tenantPackage == null) {
return fail("套餐不存在", null);
}
return success(tenantPackage);
}
@Operation(summary = "根据版本号查询套餐")
@GetMapping("/version/{version}")
public ApiResult<TenantPackage> getByVersion(@PathVariable("version") Integer version) {
TenantPackage tenantPackage = tenantPackageService.getByVersion(version);
if (tenantPackage == null) {
return fail("套餐不存在", null);
}
return success(tenantPackage);
}
}

View File

@@ -0,0 +1,183 @@
package com.gxwebsoft.common.system.controller;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.TenantSubscription;
import com.gxwebsoft.common.system.entity.TenantSubscriptionOrder;
import com.gxwebsoft.common.system.service.TenantSubscriptionOrderService;
import com.gxwebsoft.common.system.service.TenantSubscriptionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 租户订阅控制器
*
* @author WebSoft
* @since 2025-12-12
*/
@Tag(name = "租户订阅管理")
@RestController
@RequestMapping("/api/system/subscription")
public class TenantSubscriptionController extends BaseController {
@Resource
private TenantSubscriptionService tenantSubscriptionService;
@Resource
private TenantSubscriptionOrderService tenantSubscriptionOrderService;
@Operation(summary = "查询当前租户订阅信息")
@GetMapping("/current")
public ApiResult<TenantSubscription> getCurrentSubscription() {
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户ID不存在", null);
}
TenantSubscription subscription = tenantSubscriptionService.getByTenantId(tenantId);
return success(subscription);
}
@Operation(summary = "检查订阅是否有效")
@GetMapping("/check")
public ApiResult<Map<String, Object>> checkSubscription() {
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户ID不存在", null);
}
boolean isValid = tenantSubscriptionService.isSubscriptionValid(tenantId);
boolean isExpired = tenantSubscriptionService.isSubscriptionExpired(tenantId);
Map<String, Object> result = new HashMap<>();
result.put("isValid", isValid);
result.put("isExpired", isExpired);
return success(result);
}
@Operation(summary = "查询订阅激活状态")
@GetMapping("/active")
public ApiResult<Map<String, Object>> getActiveStatus() {
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户ID不存在", null);
}
TenantSubscription subscription = tenantSubscriptionService.getByTenantId(tenantId);
Map<String, Object> result = new HashMap<>();
if (subscription == null) {
result.put("active", false);
result.put("message", "未订阅任何套餐");
} else {
boolean isActive = subscription.getStatus() == 1 && subscription.getIsExpired() == 0;
result.put("active", isActive);
result.put("version", subscription.getVersion());
result.put("packageId", subscription.getPackageId());
result.put("endTime", subscription.getEndTime());
result.put("isTrial", subscription.getIsTrial());
}
return success(result);
}
@Operation(summary = "创建试用订单")
@PostMapping("/trial")
public ApiResult<TenantSubscriptionOrder> createTrialOrder() {
Integer tenantId = getTenantId();
Integer userId = getLoginUserId();
if (tenantId == null || userId == null) {
return fail("参数错误", null);
}
TenantSubscriptionOrder order = tenantSubscriptionOrderService.createTrialOrder(tenantId, userId);
return success("试用开通成功", order);
}
@Operation(summary = "创建订阅订单")
@PostMapping("/order")
public ApiResult<TenantSubscriptionOrder> createOrder(@RequestParam Integer packageId,
@RequestParam Integer payType) {
Integer tenantId = getTenantId();
Integer userId = getLoginUserId();
if (tenantId == null || userId == null) {
return fail("参数错误", null);
}
TenantSubscriptionOrder order = tenantSubscriptionOrderService.createOrder(packageId, payType, tenantId, userId);
return success("订单创建成功", order);
}
@Operation(summary = "查询订单列表")
@GetMapping("/orders")
public ApiResult<PageResult<TenantSubscriptionOrder>> listOrders(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit) {
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户ID不存在", null);
}
PageResult<TenantSubscriptionOrder> result = tenantSubscriptionOrderService.pageByTenant(tenantId, page, limit);
return success(result);
}
@Operation(summary = "根据订单号查询订单")
@GetMapping("/order/{orderNo}")
public ApiResult<TenantSubscriptionOrder> getOrderByNo(@PathVariable String orderNo) {
TenantSubscriptionOrder order = tenantSubscriptionOrderService.getByOrderNo(orderNo);
if (order == null) {
return fail("订单不存在", null);
}
return success(order);
}
@Operation(summary = "取消订单")
@PostMapping("/order/cancel")
public ApiResult<?> cancelOrder(@RequestParam String orderNo,
@RequestParam(required = false) String cancelReason) {
boolean success = tenantSubscriptionOrderService.cancelOrder(orderNo, cancelReason);
if (success) {
return success("订单已取消");
}
return fail("取消失败", null);
}
@Operation(summary = "设置自动续费")
@PostMapping("/auto-renewal")
public ApiResult<?> setAutoRenewal(@RequestParam Integer autoRenewal,
@RequestParam(required = false) Integer renewalPackageId) {
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户ID不存在", null);
}
boolean success = tenantSubscriptionService.setAutoRenewal(tenantId, autoRenewal, renewalPackageId);
if (success) {
return success(autoRenewal == 1 ? "自动续费已开启" : "自动续费已关闭");
}
return fail("设置失败", null);
}
@Operation(summary = "升级套餐")
@PostMapping("/upgrade")
public ApiResult<?> upgradeSubscription(@RequestParam Integer newPackageId) {
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户ID不存在", null);
}
boolean success = tenantSubscriptionService.upgradeSubscription(tenantId, newPackageId);
if (success) {
return success("升级成功");
}
return fail("升级失败", null);
}
}

View File

@@ -46,6 +46,18 @@ public class Tenant implements Serializable {
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "当前订阅ID")
private Long subscriptionId;
@Schema(description = "是否试用中")
private Integer isTrial;
@Schema(description = "试用结束时间")
private Date trialEndTime;
@Schema(description = "自动续费")
private Integer autoRenewal;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;

View File

@@ -0,0 +1,73 @@
package com.gxwebsoft.common.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 租户套餐
*
* @author WebSoft
* @since 2025-12-12
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "TenantPackage对象", description = "租户套餐")
@TableName("sys_tenant_package")
public class TenantPackage implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "套餐ID")
@TableId(value = "package_id", type = IdType.AUTO)
private Integer packageId;
@Schema(description = "套餐名称")
private String packageName;
@Schema(description = "版本号 0试用 10基础 20专业 30企业 99私有")
private Integer version;
@Schema(description = "月付价格")
private BigDecimal priceMonthly;
@Schema(description = "年付价格")
private BigDecimal priceYearly;
@Schema(description = "季付价格")
private BigDecimal priceQuarterly;
@Schema(description = "最大用户数 0无限")
private Integer maxUsers;
@Schema(description = "最大订单数 0无限")
private Integer maxOrders;
@Schema(description = "存储空间限制(MB) 0无限")
private Long storageLimit;
@Schema(description = "API调用限制(次/天) 0无限")
private Integer apiLimit;
@Schema(description = "试用天数")
private Integer trialDays;
@Schema(description = "功能列表JSON")
private String features;
@Schema(description = "状态 0下架 1上架")
private Integer status;
@Schema(description = "排序号")
private Integer sortNumber;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "修改时间")
private Date updateTime;
}

View File

@@ -0,0 +1,75 @@
package com.gxwebsoft.common.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* 租户订阅记录
*
* @author WebSoft
* @since 2025-12-12
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "TenantSubscription对象", description = "租户订阅记录")
@TableName("sys_tenant_subscription")
public class TenantSubscription implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订阅ID")
@TableId(value = "subscription_id", type = IdType.AUTO)
private Long subscriptionId;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "当前套餐ID")
private Integer packageId;
@Schema(description = "当前版本")
private Integer version;
@Schema(description = "开始时间")
private Date startTime;
@Schema(description = "到期时间")
private Date endTime;
@Schema(description = "是否试用期")
private Integer isTrial;
@Schema(description = "是否已过期 0否 1是")
private Integer isExpired;
@Schema(description = "自动续费 0关闭 1开启")
private Integer autoRenewal;
@Schema(description = "续费套餐ID")
private Integer renewalPackageId;
@Schema(description = "提醒状态 0未提醒 1已提醒30天 2已提醒7天 3已提醒1天")
private Integer notifyStatus;
@Schema(description = "状态 0停用 1正常")
private Integer status;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "修改时间")
private Date updateTime;
// 关联查询字段
@Schema(description = "套餐信息")
@TableField(exist = false)
private TenantPackage tenantPackage;
@Schema(description = "租户信息")
@TableField(exist = false)
private Tenant tenant;
}

View File

@@ -0,0 +1,97 @@
package com.gxwebsoft.common.system.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 租户订阅订单
*
* @author WebSoft
* @since 2025-12-12
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "TenantSubscriptionOrder对象", description = "租户订阅订单")
@TableName("sys_tenant_subscription_order")
public class TenantSubscriptionOrder implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订单ID")
@TableId(value = "order_id", type = IdType.AUTO)
private Long orderId;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "套餐ID")
private Integer packageId;
@Schema(description = "套餐名称")
private String packageName;
@Schema(description = "版本号")
private Integer version;
@Schema(description = "支付周期 1月付 3季付 12年付")
private Integer payType;
@Schema(description = "原价")
private BigDecimal originalPrice;
@Schema(description = "优惠金额")
private BigDecimal discountPrice;
@Schema(description = "实付金额")
private BigDecimal actualPrice;
@Schema(description = "开始时间")
private Date startTime;
@Schema(description = "到期时间")
private Date endTime;
@Schema(description = "是否试用 0否 1是")
private Integer isTrial;
@Schema(description = "是否续费 0首购 1续费")
private Integer isRenewal;
@Schema(description = "是否升级 0否 1是")
private Integer isUpgrade;
@Schema(description = "升级前版本")
private Integer oldVersion;
@Schema(description = "支付方式 wxpay/alipay/balance")
private String paymentMethod;
@Schema(description = "支付流水号")
private String paymentId;
@Schema(description = "支付时间")
private Date paymentTime;
@Schema(description = "订单状态 0待支付 1已支付 2已激活 3已取消 4已退款")
private Integer orderStatus;
@Schema(description = "取消原因")
private String cancelReason;
@Schema(description = "操作用户ID")
private Integer userId;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "修改时间")
private Date updateTime;
}

View File

@@ -0,0 +1,31 @@
package com.gxwebsoft.common.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.common.system.entity.TenantPackage;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 租户套餐Mapper
*
* @author WebSoft
* @since 2025-12-12
*/
public interface TenantPackageMapper extends BaseMapper<TenantPackage> {
/**
* 查询所有上架的套餐
*
* @return List<TenantPackage>
*/
List<TenantPackage> selectAvailablePackages();
/**
* 根据版本号查询套餐
*
* @param version 版本号
* @return TenantPackage
*/
TenantPackage selectByVersion(@Param("version") Integer version);
}

View File

@@ -0,0 +1,44 @@
package com.gxwebsoft.common.system.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.common.system.entity.TenantSubscription;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 租户订阅记录Mapper
*
* @author WebSoft
* @since 2025-12-12
*/
public interface TenantSubscriptionMapper extends BaseMapper<TenantSubscription> {
/**
* 根据租户ID查询订阅信息关联查询
*
* @param tenantId 租户ID
* @return TenantSubscription
*/
@InterceptorIgnore(tenantLine = "true")
TenantSubscription selectByTenantIdRel(@Param("tenantId") Integer tenantId);
/**
* 查询即将过期的订阅(用于提醒)
*
* @param days 提前天数
* @return List<TenantSubscription>
*/
@InterceptorIgnore(tenantLine = "true")
List<TenantSubscription> selectExpiringSoon(@Param("days") Integer days);
/**
* 查询已过期的订阅
*
* @return List<TenantSubscription>
*/
@InterceptorIgnore(tenantLine = "true")
List<TenantSubscription> selectExpired();
}

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.common.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.common.system.entity.TenantSubscriptionOrder;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 租户订阅订单Mapper
*
* @author WebSoft
* @since 2025-12-12
*/
public interface TenantSubscriptionOrderMapper extends BaseMapper<TenantSubscriptionOrder> {
/**
* 分页查询订单
*
* @param page 分页对象
* @param tenantId 租户ID
* @return List<TenantSubscriptionOrder>
*/
List<TenantSubscriptionOrder> selectPageByTenant(@Param("page") IPage<TenantSubscriptionOrder> page,
@Param("tenantId") Integer tenantId);
/**
* 根据订单号查询
*
* @param orderNo 订单号
* @return TenantSubscriptionOrder
*/
TenantSubscriptionOrder selectByOrderNo(@Param("orderNo") String orderNo);
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.gxwebsoft.common.system.mapper.TenantPackageMapper">
<!-- 查询所有上架的套餐 -->
<select id="selectAvailablePackages" resultType="com.gxwebsoft.common.system.entity.TenantPackage">
SELECT *
FROM sys_tenant_package
WHERE status = 1
ORDER BY sort_number ASC, version ASC
</select>
<!-- 根据版本号查询套餐 -->
<select id="selectByVersion" resultType="com.gxwebsoft.common.system.entity.TenantPackage">
SELECT *
FROM sys_tenant_package
WHERE version = #{version}
AND status = 1
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.gxwebsoft.common.system.mapper.TenantSubscriptionMapper">
<!-- 根据租户ID查询订阅信息关联查询 -->
<select id="selectByTenantIdRel" resultType="com.gxwebsoft.common.system.entity.TenantSubscription">
SELECT s.*,
p.package_name,
p.version,
p.max_users,
p.max_orders,
p.storage_limit,
p.api_limit,
p.features
FROM sys_tenant_subscription s
LEFT JOIN sys_tenant_package p ON s.package_id = p.package_id
WHERE s.tenant_id = #{tenantId}
AND s.status = 1
LIMIT 1
</select>
<!-- 查询即将过期的订阅(用于提醒) -->
<select id="selectExpiringSoon" resultType="com.gxwebsoft.common.system.entity.TenantSubscription">
SELECT s.*,
t.tenant_name,
t.phone
FROM sys_tenant_subscription s
LEFT JOIN sys_tenant t ON s.tenant_id = t.tenant_id
WHERE s.status = 1
AND s.is_expired = 0
AND s.end_time BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL #{days} DAY)
ORDER BY s.end_time ASC
</select>
<!-- 查询已过期的订阅 -->
<select id="selectExpired" resultType="com.gxwebsoft.common.system.entity.TenantSubscription">
SELECT s.*
FROM sys_tenant_subscription s
WHERE s.status = 1
AND s.is_expired = 0
AND s.end_time &lt; NOW()
</select>
</mapper>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.gxwebsoft.common.system.mapper.TenantSubscriptionOrderMapper">
<!-- 分页查询订单 -->
<select id="selectPageByTenant" resultType="com.gxwebsoft.common.system.entity.TenantSubscriptionOrder">
SELECT *
FROM sys_tenant_subscription_order
WHERE tenant_id = #{tenantId}
ORDER BY create_time DESC
</select>
<!-- 根据订单号查询 -->
<select id="selectByOrderNo" resultType="com.gxwebsoft.common.system.entity.TenantSubscriptionOrder">
SELECT *
FROM sys_tenant_subscription_order
WHERE order_no = #{orderNo}
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,30 @@
package com.gxwebsoft.common.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.system.entity.TenantPackage;
import java.util.List;
/**
* 租户套餐Service
*
* @author WebSoft
* @since 2025-12-12
*/
public interface TenantPackageService extends IService<TenantPackage> {
/**
* 查询所有上架的套餐
*
* @return List<TenantPackage>
*/
List<TenantPackage> listAvailablePackages();
/**
* 根据版本号查询套餐
*
* @param version 版本号
* @return TenantPackage
*/
TenantPackage getByVersion(Integer version);
}

View File

@@ -0,0 +1,79 @@
package com.gxwebsoft.common.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.TenantSubscriptionOrder;
/**
* 租户订阅订单Service
*
* @author WebSoft
* @since 2025-12-12
*/
public interface TenantSubscriptionOrderService extends IService<TenantSubscriptionOrder> {
/**
* 创建订阅订单
*
* @param packageId 套餐ID
* @param payType 支付周期 1月付 3季付 12年付
* @param tenantId 租户ID
* @param userId 用户ID
* @return TenantSubscriptionOrder
*/
TenantSubscriptionOrder createOrder(Integer packageId, Integer payType, Integer tenantId, Integer userId);
/**
* 创建试用订单
*
* @param tenantId 租户ID
* @param userId 用户ID
* @return TenantSubscriptionOrder
*/
TenantSubscriptionOrder createTrialOrder(Integer tenantId, Integer userId);
/**
* 支付订单
*
* @param orderNo 订单号
* @param paymentMethod 支付方式
* @param paymentId 支付流水号
* @return boolean
*/
boolean payOrder(String orderNo, String paymentMethod, String paymentId);
/**
* 激活订单(支付成功后调用)
*
* @param orderNo 订单号
* @return boolean
*/
boolean activateOrder(String orderNo);
/**
* 取消订单
*
* @param orderNo 订单号
* @param cancelReason 取消原因
* @return boolean
*/
boolean cancelOrder(String orderNo, String cancelReason);
/**
* 分页查询租户订单
*
* @param tenantId 租户ID
* @param page 页码
* @param limit 每页数量
* @return PageResult<TenantSubscriptionOrder>
*/
PageResult<TenantSubscriptionOrder> pageByTenant(Integer tenantId, Integer page, Integer limit);
/**
* 根据订单号查询
*
* @param orderNo 订单号
* @return TenantSubscriptionOrder
*/
TenantSubscriptionOrder getByOrderNo(String orderNo);
}

View File

@@ -0,0 +1,113 @@
package com.gxwebsoft.common.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.system.entity.TenantSubscription;
import java.util.List;
/**
* 租户订阅记录Service
*
* @author WebSoft
* @since 2025-12-12
*/
public interface TenantSubscriptionService extends IService<TenantSubscription> {
/**
* 根据租户ID查询订阅信息
*
* @param tenantId 租户ID
* @return TenantSubscription
*/
TenantSubscription getByTenantId(Integer tenantId);
/**
* 创建或更新订阅
*
* @param tenantId 租户ID
* @param packageId 套餐ID
* @param version 版本号
* @param startTime 开始时间
* @param endTime 到期时间
* @param isTrial 是否试用
* @return TenantSubscription
*/
TenantSubscription createOrUpdateSubscription(Integer tenantId, Integer packageId, Integer version,
java.util.Date startTime, java.util.Date endTime, Integer isTrial);
/**
* 升级订阅
*
* @param tenantId 租户ID
* @param newPackageId 新套餐ID
* @return boolean
*/
boolean upgradeSubscription(Integer tenantId, Integer newPackageId);
/**
* 续费订阅
*
* @param tenantId 租户ID
* @param months 续费月数
* @return boolean
*/
boolean renewSubscription(Integer tenantId, Integer months);
/**
* 设置自动续费
*
* @param tenantId 租户ID
* @param autoRenewal 是否自动续费
* @param renewalPackageId 续费套餐ID
* @return boolean
*/
boolean setAutoRenewal(Integer tenantId, Integer autoRenewal, Integer renewalPackageId);
/**
* 检查订阅是否有效
*
* @param tenantId 租户ID
* @return boolean
*/
boolean isSubscriptionValid(Integer tenantId);
/**
* 检查订阅是否过期
*
* @param tenantId 租户ID
* @return boolean
*/
boolean isSubscriptionExpired(Integer tenantId);
/**
* 查询即将过期的订阅
*
* @param days 提前天数
* @return List<TenantSubscription>
*/
List<TenantSubscription> listExpiringSoon(Integer days);
/**
* 查询已过期的订阅
*
* @return List<TenantSubscription>
*/
List<TenantSubscription> listExpired();
/**
* 标记订阅为已过期
*
* @param subscriptionId 订阅ID
* @return boolean
*/
boolean markAsExpired(Long subscriptionId);
/**
* 发送过期提醒
*
* @param subscriptionId 订阅ID
* @param notifyStatus 提醒状态
* @return boolean
*/
boolean sendExpiryNotification(Long subscriptionId, Integer notifyStatus);
}

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.common.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.system.entity.TenantPackage;
import com.gxwebsoft.common.system.mapper.TenantPackageMapper;
import com.gxwebsoft.common.system.service.TenantPackageService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 租户套餐ServiceImpl
*
* @author WebSoft
* @since 2025-12-12
*/
@Service
public class TenantPackageServiceImpl extends ServiceImpl<TenantPackageMapper, TenantPackage> implements TenantPackageService {
@Override
public List<TenantPackage> listAvailablePackages() {
return baseMapper.selectAvailablePackages();
}
@Override
public TenantPackage getByVersion(Integer version) {
return baseMapper.selectByVersion(version);
}
}

View File

@@ -0,0 +1,230 @@
package com.gxwebsoft.common.system.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.TenantPackage;
import com.gxwebsoft.common.system.entity.TenantSubscriptionOrder;
import com.gxwebsoft.common.system.mapper.TenantSubscriptionOrderMapper;
import com.gxwebsoft.common.system.service.TenantPackageService;
import com.gxwebsoft.common.system.service.TenantSubscriptionOrderService;
import com.gxwebsoft.common.system.service.TenantSubscriptionService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
/**
* 租户订阅订单ServiceImpl
*
* @author WebSoft
* @since 2025-12-12
*/
@Service
public class TenantSubscriptionOrderServiceImpl extends ServiceImpl<TenantSubscriptionOrderMapper, TenantSubscriptionOrder> implements TenantSubscriptionOrderService {
@Resource
private TenantPackageService tenantPackageService;
@Resource
private TenantSubscriptionService tenantSubscriptionService;
@Override
@Transactional(rollbackFor = Exception.class)
public TenantSubscriptionOrder createOrder(Integer packageId, Integer payType, Integer tenantId, Integer userId) {
// 查询套餐信息
TenantPackage tenantPackage = tenantPackageService.getById(packageId);
if (tenantPackage == null) {
throw new BusinessException("套餐不存在");
}
if (tenantPackage.getStatus() != 1) {
throw new BusinessException("套餐已下架");
}
// 计算价格
BigDecimal price;
if (payType == 1) {
price = tenantPackage.getPriceMonthly();
} else if (payType == 3) {
price = tenantPackage.getPriceQuarterly();
} else if (payType == 12) {
price = tenantPackage.getPriceYearly();
} else {
throw new BusinessException("支付周期不正确");
}
// 创建订单
TenantSubscriptionOrder order = new TenantSubscriptionOrder();
order.setOrderNo(generateOrderNo());
order.setTenantId(tenantId);
order.setPackageId(packageId);
order.setPackageName(tenantPackage.getPackageName());
order.setVersion(tenantPackage.getVersion());
order.setPayType(payType);
order.setOriginalPrice(price);
order.setDiscountPrice(BigDecimal.ZERO);
order.setActualPrice(price);
order.setIsTrial(0);
order.setIsRenewal(0);
order.setIsUpgrade(0);
order.setOrderStatus(0); // 待支付
order.setUserId(userId);
order.setCreateTime(new Date());
save(order);
return order;
}
@Override
@Transactional(rollbackFor = Exception.class)
public TenantSubscriptionOrder createTrialOrder(Integer tenantId, Integer userId) {
// 查询试用套餐version=0
TenantPackage trialPackage = tenantPackageService.getByVersion(0);
if (trialPackage == null) {
throw new BusinessException("试用套餐不存在");
}
// 检查是否已经试用过
long count = count(new LambdaQueryWrapper<TenantSubscriptionOrder>()
.eq(TenantSubscriptionOrder::getTenantId, tenantId)
.eq(TenantSubscriptionOrder::getIsTrial, 1));
if (count > 0) {
throw new BusinessException("已经使用过试用版");
}
// 创建试用订单
TenantSubscriptionOrder order = new TenantSubscriptionOrder();
order.setOrderNo(generateOrderNo());
order.setTenantId(tenantId);
order.setPackageId(trialPackage.getPackageId());
order.setPackageName(trialPackage.getPackageName());
order.setVersion(trialPackage.getVersion());
order.setPayType(0);
order.setOriginalPrice(BigDecimal.ZERO);
order.setDiscountPrice(BigDecimal.ZERO);
order.setActualPrice(BigDecimal.ZERO);
order.setStartTime(new Date());
order.setEndTime(DateUtil.offsetDay(new Date(), trialPackage.getTrialDays()));
order.setIsTrial(1);
order.setIsRenewal(0);
order.setIsUpgrade(0);
order.setOrderStatus(2); // 直接激活
order.setUserId(userId);
order.setCreateTime(new Date());
save(order);
// 创建订阅记录
tenantSubscriptionService.createOrUpdateSubscription(
tenantId,
trialPackage.getPackageId(),
trialPackage.getVersion(),
order.getStartTime(),
order.getEndTime(),
1
);
return order;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean payOrder(String orderNo, String paymentMethod, String paymentId) {
TenantSubscriptionOrder order = getByOrderNo(orderNo);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (order.getOrderStatus() != 0) {
throw new BusinessException("订单状态不正确");
}
order.setPaymentMethod(paymentMethod);
order.setPaymentId(paymentId);
order.setPaymentTime(new Date());
order.setOrderStatus(1); // 已支付
order.setUpdateTime(new Date());
return updateById(order);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean activateOrder(String orderNo) {
TenantSubscriptionOrder order = getByOrderNo(orderNo);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (order.getOrderStatus() != 1) {
throw new BusinessException("订单未支付");
}
// 计算开始和结束时间
Date startTime = new Date();
Date endTime = DateUtil.offsetMonth(startTime, order.getPayType());
order.setStartTime(startTime);
order.setEndTime(endTime);
order.setOrderStatus(2); // 已激活
order.setUpdateTime(new Date());
boolean updated = updateById(order);
if (updated) {
// 创建或更新订阅记录
tenantSubscriptionService.createOrUpdateSubscription(
order.getTenantId(),
order.getPackageId(),
order.getVersion(),
startTime,
endTime,
0
);
}
return updated;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelOrder(String orderNo, String cancelReason) {
TenantSubscriptionOrder order = getByOrderNo(orderNo);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (order.getOrderStatus() != 0) {
throw new BusinessException("只能取消待支付订单");
}
order.setOrderStatus(3); // 已取消
order.setCancelReason(cancelReason);
order.setUpdateTime(new Date());
return updateById(order);
}
@Override
public PageResult<TenantSubscriptionOrder> pageByTenant(Integer tenantId, Integer page, Integer limit) {
IPage<TenantSubscriptionOrder> iPage = new Page<>(page, limit);
baseMapper.selectPageByTenant(iPage, tenantId);
return new PageResult<TenantSubscriptionOrder>(iPage.getRecords(), iPage.getTotal());
}
@Override
public TenantSubscriptionOrder getByOrderNo(String orderNo) {
return baseMapper.selectByOrderNo(orderNo);
}
/**
* 生成订单号
*/
private String generateOrderNo() {
return "SUB" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + IdUtil.randomUUID().substring(0, 6).toUpperCase();
}
}

View File

@@ -0,0 +1,188 @@
package com.gxwebsoft.common.system.service.impl;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.system.entity.TenantPackage;
import com.gxwebsoft.common.system.entity.TenantSubscription;
import com.gxwebsoft.common.system.mapper.TenantSubscriptionMapper;
import com.gxwebsoft.common.system.service.TenantPackageService;
import com.gxwebsoft.common.system.service.TenantSubscriptionService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* 租户订阅记录ServiceImpl
*
* @author WebSoft
* @since 2025-12-12
*/
@Service
public class TenantSubscriptionServiceImpl extends ServiceImpl<TenantSubscriptionMapper, TenantSubscription> implements TenantSubscriptionService {
@Resource
private TenantPackageService tenantPackageService;
@Override
public TenantSubscription getByTenantId(Integer tenantId) {
return baseMapper.selectByTenantIdRel(tenantId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public TenantSubscription createOrUpdateSubscription(Integer tenantId, Integer packageId, Integer version,
Date startTime, Date endTime, Integer isTrial) {
// 查询是否已存在订阅
TenantSubscription subscription = getOne(new LambdaQueryWrapper<TenantSubscription>()
.eq(TenantSubscription::getTenantId, tenantId));
if (subscription == null) {
// 创建新订阅
subscription = new TenantSubscription();
subscription.setTenantId(tenantId);
subscription.setPackageId(packageId);
subscription.setVersion(version);
subscription.setStartTime(startTime);
subscription.setEndTime(endTime);
subscription.setIsTrial(isTrial);
subscription.setIsExpired(0);
subscription.setAutoRenewal(0);
subscription.setNotifyStatus(0);
subscription.setStatus(1);
subscription.setCreateTime(new Date());
save(subscription);
} else {
// 更新订阅
subscription.setPackageId(packageId);
subscription.setVersion(version);
subscription.setStartTime(startTime);
subscription.setEndTime(endTime);
subscription.setIsTrial(isTrial);
subscription.setIsExpired(0);
subscription.setNotifyStatus(0);
subscription.setUpdateTime(new Date());
updateById(subscription);
}
return subscription;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean upgradeSubscription(Integer tenantId, Integer newPackageId) {
TenantSubscription subscription = getByTenantId(tenantId);
if (subscription == null) {
throw new BusinessException("订阅不存在");
}
TenantPackage newPackage = tenantPackageService.getById(newPackageId);
if (newPackage == null) {
throw new BusinessException("套餐不存在");
}
// 检查是否是升级
if (newPackage.getVersion() <= subscription.getVersion()) {
throw new BusinessException("只能升级到更高版本");
}
subscription.setPackageId(newPackageId);
subscription.setVersion(newPackage.getVersion());
subscription.setUpdateTime(new Date());
return updateById(subscription);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean renewSubscription(Integer tenantId, Integer months) {
TenantSubscription subscription = getByTenantId(tenantId);
if (subscription == null) {
throw new BusinessException("订阅不存在");
}
// 从当前到期时间延长
Date newEndTime = DateUtil.offsetMonth(subscription.getEndTime(), months);
subscription.setEndTime(newEndTime);
subscription.setIsExpired(0);
subscription.setUpdateTime(new Date());
return updateById(subscription);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean setAutoRenewal(Integer tenantId, Integer autoRenewal, Integer renewalPackageId) {
TenantSubscription subscription = getByTenantId(tenantId);
if (subscription == null) {
throw new BusinessException("订阅不存在");
}
subscription.setAutoRenewal(autoRenewal);
subscription.setRenewalPackageId(renewalPackageId);
subscription.setUpdateTime(new Date());
return updateById(subscription);
}
@Override
public boolean isSubscriptionValid(Integer tenantId) {
TenantSubscription subscription = getByTenantId(tenantId);
if (subscription == null) {
return false;
}
return subscription.getStatus() == 1 && subscription.getIsExpired() == 0
&& subscription.getEndTime().after(new Date());
}
@Override
public boolean isSubscriptionExpired(Integer tenantId) {
TenantSubscription subscription = getByTenantId(tenantId);
if (subscription == null) {
return true;
}
return subscription.getIsExpired() == 1 || subscription.getEndTime().before(new Date());
}
@Override
public List<TenantSubscription> listExpiringSoon(Integer days) {
return baseMapper.selectExpiringSoon(days);
}
@Override
public List<TenantSubscription> listExpired() {
return baseMapper.selectExpired();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean markAsExpired(Long subscriptionId) {
TenantSubscription subscription = getById(subscriptionId);
if (subscription == null) {
return false;
}
subscription.setIsExpired(1);
subscription.setUpdateTime(new Date());
return updateById(subscription);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean sendExpiryNotification(Long subscriptionId, Integer notifyStatus) {
TenantSubscription subscription = getById(subscriptionId);
if (subscription == null) {
return false;
}
subscription.setNotifyStatus(notifyStatus);
subscription.setUpdateTime(new Date());
return updateById(subscription);
}
}

View File

@@ -5,14 +5,14 @@ spring:
main:
allow-circular-references: true
datasource:
url: jdbc:mysql://132.232.214.96:13306/gxwebsoft_core?useSSL=false&serverTimezone=UTC
url: jdbc:mysql://47.119.165.234:13308/gxwebsoft_core?useSSL=false&serverTimezone=UTC
username: gxwebsoft_core
password: jdj7HYEdYHnYEFBy
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
redis:
database: 0
host: 132.232.214.96
host: 47.119.165.234
port: 16379
password: redis_WSDb88