From 980f8f187ecaab7ee7c1e83a988c7147b2c8bea7 Mon Sep 17 00:00:00 2001 From: gxwebsoft <170083662@qq.com> Date: Fri, 12 Dec 2025 16:35:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenant):=20=E6=B7=BB=E5=8A=A0=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=90=8D=E7=A7=B0=E9=87=8D=E5=A4=8D=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在创建租户时检查名称是否已存在 - 确保租户名称不为空 - 防止重复租户名称导致的数据冲突 --- .../controller/TenantPackageController.java | 54 ++++ .../TenantSubscriptionController.java | 183 ++++++++++++++ .../common/system/entity/Tenant.java | 12 + .../common/system/entity/TenantPackage.java | 73 ++++++ .../system/entity/TenantSubscription.java | 75 ++++++ .../entity/TenantSubscriptionOrder.java | 97 ++++++++ .../system/mapper/TenantPackageMapper.java | 31 +++ .../mapper/TenantSubscriptionMapper.java | 44 ++++ .../mapper/TenantSubscriptionOrderMapper.java | 35 +++ .../system/mapper/xml/TenantPackageMapper.xml | 22 ++ .../mapper/xml/TenantSubscriptionMapper.xml | 44 ++++ .../xml/TenantSubscriptionOrderMapper.xml | 21 ++ .../system/service/TenantPackageService.java | 30 +++ .../TenantSubscriptionOrderService.java | 79 ++++++ .../service/TenantSubscriptionService.java | 113 +++++++++ .../impl/TenantPackageServiceImpl.java | 29 +++ .../TenantSubscriptionOrderServiceImpl.java | 230 ++++++++++++++++++ .../impl/TenantSubscriptionServiceImpl.java | 188 ++++++++++++++ src/main/resources/application-dev.yml | 4 +- 19 files changed, 1362 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/common/system/controller/TenantPackageController.java create mode 100644 src/main/java/com/gxwebsoft/common/system/controller/TenantSubscriptionController.java create mode 100644 src/main/java/com/gxwebsoft/common/system/entity/TenantPackage.java create mode 100644 src/main/java/com/gxwebsoft/common/system/entity/TenantSubscription.java create mode 100644 src/main/java/com/gxwebsoft/common/system/entity/TenantSubscriptionOrder.java create mode 100644 src/main/java/com/gxwebsoft/common/system/mapper/TenantPackageMapper.java create mode 100644 src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionMapper.java create mode 100644 src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionOrderMapper.java create mode 100644 src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantPackageMapper.xml create mode 100644 src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionMapper.xml create mode 100644 src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionOrderMapper.xml create mode 100644 src/main/java/com/gxwebsoft/common/system/service/TenantPackageService.java create mode 100644 src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionOrderService.java create mode 100644 src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionService.java create mode 100644 src/main/java/com/gxwebsoft/common/system/service/impl/TenantPackageServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionOrderServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionServiceImpl.java diff --git a/src/main/java/com/gxwebsoft/common/system/controller/TenantPackageController.java b/src/main/java/com/gxwebsoft/common/system/controller/TenantPackageController.java new file mode 100644 index 0000000..96a4bc8 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/controller/TenantPackageController.java @@ -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() { + List packages = tenantPackageService.listAvailablePackages(); + return success(packages); + } + + @Operation(summary = "根据ID查询套餐详情") + @GetMapping("/{id}") + public ApiResult 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 getByVersion(@PathVariable("version") Integer version) { + TenantPackage tenantPackage = tenantPackageService.getByVersion(version); + if (tenantPackage == null) { + return fail("套餐不存在", null); + } + return success(tenantPackage); + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/controller/TenantSubscriptionController.java b/src/main/java/com/gxwebsoft/common/system/controller/TenantSubscriptionController.java new file mode 100644 index 0000000..c2fe60d --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/controller/TenantSubscriptionController.java @@ -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 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> checkSubscription() { + Integer tenantId = getTenantId(); + if (tenantId == null) { + return fail("租户ID不存在", null); + } + + boolean isValid = tenantSubscriptionService.isSubscriptionValid(tenantId); + boolean isExpired = tenantSubscriptionService.isSubscriptionExpired(tenantId); + + Map result = new HashMap<>(); + result.put("isValid", isValid); + result.put("isExpired", isExpired); + + return success(result); + } + + @Operation(summary = "查询订阅激活状态") + @GetMapping("/active") + public ApiResult> getActiveStatus() { + Integer tenantId = getTenantId(); + if (tenantId == null) { + return fail("租户ID不存在", null); + } + + TenantSubscription subscription = tenantSubscriptionService.getByTenantId(tenantId); + Map 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 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 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> listOrders(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer limit) { + Integer tenantId = getTenantId(); + if (tenantId == null) { + return fail("租户ID不存在", null); + } + + PageResult result = tenantSubscriptionOrderService.pageByTenant(tenantId, page, limit); + return success(result); + } + + @Operation(summary = "根据订单号查询订单") + @GetMapping("/order/{orderNo}") + public ApiResult 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); + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/entity/Tenant.java b/src/main/java/com/gxwebsoft/common/system/entity/Tenant.java index 44868b0..ac096af 100644 --- a/src/main/java/com/gxwebsoft/common/system/entity/Tenant.java +++ b/src/main/java/com/gxwebsoft/common/system/entity/Tenant.java @@ -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; diff --git a/src/main/java/com/gxwebsoft/common/system/entity/TenantPackage.java b/src/main/java/com/gxwebsoft/common/system/entity/TenantPackage.java new file mode 100644 index 0000000..3aa1f5c --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/entity/TenantPackage.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/entity/TenantSubscription.java b/src/main/java/com/gxwebsoft/common/system/entity/TenantSubscription.java new file mode 100644 index 0000000..4ef6c3b --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/entity/TenantSubscription.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/entity/TenantSubscriptionOrder.java b/src/main/java/com/gxwebsoft/common/system/entity/TenantSubscriptionOrder.java new file mode 100644 index 0000000..f92f56e --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/entity/TenantSubscriptionOrder.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/TenantPackageMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/TenantPackageMapper.java new file mode 100644 index 0000000..4780945 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/TenantPackageMapper.java @@ -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 { + + /** + * 查询所有上架的套餐 + * + * @return List + */ + List selectAvailablePackages(); + + /** + * 根据版本号查询套餐 + * + * @param version 版本号 + * @return TenantPackage + */ + TenantPackage selectByVersion(@Param("version") Integer version); +} diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionMapper.java new file mode 100644 index 0000000..1479181 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionMapper.java @@ -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 { + + /** + * 根据租户ID查询订阅信息(关联查询) + * + * @param tenantId 租户ID + * @return TenantSubscription + */ + @InterceptorIgnore(tenantLine = "true") + TenantSubscription selectByTenantIdRel(@Param("tenantId") Integer tenantId); + + /** + * 查询即将过期的订阅(用于提醒) + * + * @param days 提前天数 + * @return List + */ + @InterceptorIgnore(tenantLine = "true") + List selectExpiringSoon(@Param("days") Integer days); + + /** + * 查询已过期的订阅 + * + * @return List + */ + @InterceptorIgnore(tenantLine = "true") + List selectExpired(); +} diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionOrderMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionOrderMapper.java new file mode 100644 index 0000000..4012675 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/TenantSubscriptionOrderMapper.java @@ -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 { + + /** + * 分页查询订单 + * + * @param page 分页对象 + * @param tenantId 租户ID + * @return List + */ + List selectPageByTenant(@Param("page") IPage page, + @Param("tenantId") Integer tenantId); + + /** + * 根据订单号查询 + * + * @param orderNo 订单号 + * @return TenantSubscriptionOrder + */ + TenantSubscriptionOrder selectByOrderNo(@Param("orderNo") String orderNo); +} diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantPackageMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantPackageMapper.xml new file mode 100644 index 0000000..3b1ded6 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantPackageMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionMapper.xml new file mode 100644 index 0000000..4255e78 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionOrderMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionOrderMapper.xml new file mode 100644 index 0000000..921084a --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/TenantSubscriptionOrderMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/service/TenantPackageService.java b/src/main/java/com/gxwebsoft/common/system/service/TenantPackageService.java new file mode 100644 index 0000000..a301d36 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/TenantPackageService.java @@ -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 { + + /** + * 查询所有上架的套餐 + * + * @return List + */ + List listAvailablePackages(); + + /** + * 根据版本号查询套餐 + * + * @param version 版本号 + * @return TenantPackage + */ + TenantPackage getByVersion(Integer version); +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionOrderService.java b/src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionOrderService.java new file mode 100644 index 0000000..e172d20 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionOrderService.java @@ -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 { + + /** + * 创建订阅订单 + * + * @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 + */ + PageResult pageByTenant(Integer tenantId, Integer page, Integer limit); + + /** + * 根据订单号查询 + * + * @param orderNo 订单号 + * @return TenantSubscriptionOrder + */ + TenantSubscriptionOrder getByOrderNo(String orderNo); +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionService.java b/src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionService.java new file mode 100644 index 0000000..cd7a5af --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/TenantSubscriptionService.java @@ -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 { + + /** + * 根据租户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 + */ + List listExpiringSoon(Integer days); + + /** + * 查询已过期的订阅 + * + * @return List + */ + List listExpired(); + + /** + * 标记订阅为已过期 + * + * @param subscriptionId 订阅ID + * @return boolean + */ + boolean markAsExpired(Long subscriptionId); + + /** + * 发送过期提醒 + * + * @param subscriptionId 订阅ID + * @param notifyStatus 提醒状态 + * @return boolean + */ + boolean sendExpiryNotification(Long subscriptionId, Integer notifyStatus); +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantPackageServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantPackageServiceImpl.java new file mode 100644 index 0000000..d89f1f9 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantPackageServiceImpl.java @@ -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 implements TenantPackageService { + + @Override + public List listAvailablePackages() { + return baseMapper.selectAvailablePackages(); + } + + @Override + public TenantPackage getByVersion(Integer version) { + return baseMapper.selectByVersion(version); + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionOrderServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionOrderServiceImpl.java new file mode 100644 index 0000000..0ab00c4 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionOrderServiceImpl.java @@ -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 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() + .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 pageByTenant(Integer tenantId, Integer page, Integer limit) { + IPage iPage = new Page<>(page, limit); + baseMapper.selectPageByTenant(iPage, tenantId); + return new PageResult(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(); + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionServiceImpl.java new file mode 100644 index 0000000..9a5e1f4 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantSubscriptionServiceImpl.java @@ -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 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() + .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 listExpiringSoon(Integer days) { + return baseMapper.selectExpiringSoon(days); + } + + @Override + public List 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); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 44313e1..bb600fe 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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