feat(backend): 补齐 C 端商业化与账号缺口(金币充值/会员/搜索/语言筛选/Google登录/短信/商店可配置接入)
This commit is contained in:
1 parent
212834d59d
commit
63b086998e
25 files changed
+859
-29
No files matched your search
+10
@@ -37,6 +37,7 @@ public class EntitlementService implements EntitlementPort {
|
||||
|
||||
public static final String TYPE_EPISODE = "EPISODE";
|
||||
public static final String TYPE_DRAMA = "DRAMA";
|
||||
public static final String TYPE_VIP = "VIP";
|
||||
|
||||
private final EntitlementMapper entitlementMapper;
|
||||
|
||||
@@ -46,6 +47,15 @@ public class EntitlementService implements EntitlementPort {
|
||||
return false;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Long vipCount = entitlementMapper.selectCount(Wrappers.<Entitlement>lambdaQuery()
|
||||
.eq(Entitlement::getUserId, userId)
|
||||
.eq(Entitlement::getStatus, STATUS_ACTIVE)
|
||||
.eq(Entitlement::getType, TYPE_VIP)
|
||||
.and(w -> w.isNull(Entitlement::getExpireAt)
|
||||
.or().gt(Entitlement::getExpireAt, now)));
|
||||
if (vipCount != null && vipCount > 0) {
|
||||
return true;
|
||||
}
|
||||
Long count = entitlementMapper.selectCount(Wrappers.<Entitlement>lambdaQuery()
|
||||
.eq(Entitlement::getUserId, userId)
|
||||
.eq(Entitlement::getStatus, STATUS_ACTIVE)
|
||||
|
||||
+47
@@ -93,6 +93,49 @@ public class OrderService {
|
||||
.orderByDesc(OrderEvent::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端创建充值订单:按商品配置生成一笔 CREATED 订单,等待后续支付。
|
||||
*
|
||||
* <p>商品必须处于上架状态;金币数量按 coinAmount + bonusCoin 计算,
|
||||
* 与商店票据链路 {@link #buildOrder} 的口径一致,交付时一次性到账。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Orders createOrder(Long userId, Long productId) {
|
||||
if (userId == null) {
|
||||
throw new ServiceException(ErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
Product product = productService.getExisting(productId);
|
||||
if (product.getStatus() == null || product.getStatus() != 1) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "商品已下架");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Orders order = new Orders();
|
||||
order.setOrderNo(newOrderNo());
|
||||
order.setUserId(userId);
|
||||
order.setProductId(product.getId());
|
||||
order.setProductCode(product.getCode());
|
||||
order.setProductName(product.getName());
|
||||
order.setType(StringUtils.hasText(product.getType()) ? product.getType() : ProductService.TYPE_COIN);
|
||||
order.setPlatform(product.getPlatform());
|
||||
order.setStoreProductId(product.getStoreProductId());
|
||||
long coins = (product.getCoinAmount() == null ? 0L : product.getCoinAmount())
|
||||
+ (product.getBonusCoin() == null ? 0L : product.getBonusCoin());
|
||||
order.setCoinAmount(coins);
|
||||
order.setAmount(product.getPriceAmount() == null ? BigDecimal.ZERO : product.getPriceAmount());
|
||||
order.setCurrency(StringUtils.hasText(product.getCurrency()) ? product.getCurrency() : "IDR");
|
||||
order.setQuantity(1);
|
||||
order.setStatus(STATUS_CREATED);
|
||||
order.setRegionCode("ID");
|
||||
order.setChannel("IAP");
|
||||
order.setExperimentId("");
|
||||
order.setReceipt("");
|
||||
order.setFailReason("");
|
||||
order.setRefundReason("");
|
||||
order.setCreatedAt(now);
|
||||
ordersMapper.insert(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端提交商店票据:**校验 → 建单 → 发权益**,一步都不能省。
|
||||
*
|
||||
@@ -273,6 +316,10 @@ public class OrderService {
|
||||
order.getOrderNo(), "order:" + order.getOrderNo() + ":coins",
|
||||
remark + ":" + order.getProductName(), true, operatorId);
|
||||
}
|
||||
if (ProductService.TYPE_VIP.equals(order.getType())) {
|
||||
entitlementService.grant(order.getUserId(), 0L, 0L,
|
||||
EntitlementService.TYPE_VIP, "ORDER", order.getOrderNo());
|
||||
}
|
||||
order.setStatus(STATUS_DELIVERED);
|
||||
order.setDeliveredAt(LocalDateTime.now());
|
||||
order.setFailReason("");
|
||||
|
||||
+15
-15
@@ -30,7 +30,7 @@ import java.util.List;
|
||||
* <li>`store_product_id` 在**同一 `platform` 下唯一** —— 否则同一商店商品被配置成两个商品,
|
||||
* 客户端 verify 时"按 storeProductId 找到第一个"会随机发其中一个的金币;</li>
|
||||
* <li>`type=COIN_PACK` 要求 `coin_amount > 0`;`type=VIP` 要求 `vip_days > 0`;</li>
|
||||
* <li>**VIP 允许建档、不允许启用**(与 B1-1「VIP 订阅本轮不做」一致)。</li>
|
||||
* <li>**VIP 允许建档、允许启用**(会员商品随金币包一起上架售卖)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@@ -64,6 +64,17 @@ public class ProductService {
|
||||
return ProductRespVO.of(getExisting(id));
|
||||
}
|
||||
|
||||
/** C 端商品列表:只返回上架商品,type 可选过滤(自动归一化为大写)。 */
|
||||
public List<ProductRespVO> listForApp(String type) {
|
||||
String normalized = StringUtils.hasText(type) ? type.trim().toUpperCase() : null;
|
||||
List<Product> records = productMapper.selectList(Wrappers.<Product>lambdaQuery()
|
||||
.eq(Product::getStatus, 1)
|
||||
.eq(normalized != null, Product::getType, normalized)
|
||||
.orderByAsc(Product::getSort)
|
||||
.orderByDesc(Product::getId));
|
||||
return records.stream().map(ProductRespVO::of).toList();
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(ProductSaveReqVO req) {
|
||||
String code = req.getCode().trim();
|
||||
@@ -77,7 +88,6 @@ public class ProductService {
|
||||
applyFields(product, req, platform);
|
||||
// 新建默认下架:上架是显式动作,避免半配置的商品立刻对客户端可见
|
||||
product.setStatus(req.getStatus() == null ? 0 : req.getStatus());
|
||||
requireVipNotEnabled(product);
|
||||
productMapper.insert(product);
|
||||
return product.getId();
|
||||
}
|
||||
@@ -96,11 +106,10 @@ public class ProductService {
|
||||
if (req.getStatus() != null) {
|
||||
product.setStatus(req.getStatus());
|
||||
}
|
||||
requireVipNotEnabled(product);
|
||||
productMapper.updateById(product);
|
||||
}
|
||||
|
||||
/** 上架 / 下架;VIP 商品只允许下架(第二阶段订阅上线后才放开)。 */
|
||||
/** 上架 / 下架。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateStatus(Long id, Integer status) {
|
||||
if (status == null || (status != 0 && status != 1)) {
|
||||
@@ -108,7 +117,6 @@ public class ProductService {
|
||||
}
|
||||
Product product = getExisting(id);
|
||||
product.setStatus(status);
|
||||
requireVipNotEnabled(product);
|
||||
productMapper.updateById(product);
|
||||
}
|
||||
|
||||
@@ -161,15 +169,6 @@ public class ProductService {
|
||||
}
|
||||
}
|
||||
|
||||
/** VIP 商品在订阅功能上线前不能启用(允许建档)。 */
|
||||
private void requireVipNotEnabled(Product product) {
|
||||
boolean enabled = product.getStatus() != null && product.getStatus() == 1;
|
||||
if (enabled && TYPE_VIP.equals(product.getType())) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST,
|
||||
"会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCodeFree(String code, Long excludeId) {
|
||||
Long count = productMapper.selectCount(Wrappers.<Product>lambdaQuery()
|
||||
.eq(Product::getCode, code)
|
||||
@@ -198,7 +197,8 @@ public class ProductService {
|
||||
}
|
||||
}
|
||||
|
||||
private Product getExisting(Long id) {
|
||||
/** 按 id 取商品,不存在抛业务异常(供订单创建链路复用)。 */
|
||||
public Product getExisting(Long id) {
|
||||
Product product = id == null ? null : productMapper.selectById(id);
|
||||
if (product == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "商品不存在");
|
||||
|
||||
Reference in new issue
Block a user