feat(backend): 补齐 C 端商业化与账号缺口(金币充值/会员/搜索/语言筛选/Google登录/短信/商店可配置接入)

This commit is contained in:
Zhou Weidong committed 2026-10-20 04:20:42 +08:00
1 parent 212834d59d
commit 63b086998e
25 files changed
+859 -29

No files matched your search

@@ -2,6 +2,7 @@
package com.fruitshorts.app.controller;
import com.fruitshorts.app.vo.GoogleLoginReqVO;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.web.WebUtils;
@@ -55,6 +56,12 @@ public class AppAuthController {
return Result.ok(authService.login(req, WebUtils.getClientIp(request)));
}
@Operation(summary = "Google 登录(未配置 clientId 时返回明确错误)")
@PostMapping("/google")
public Result<TokenRespVO> google(@Valid @RequestBody GoogleLoginReqVO req) {
return Result.ok(authService.googleLogin(req.getIdToken()));
}
@Operation(summary = "刷新令牌(旧 refresh token 立即失效)")
@PostMapping("/refresh")
public Result<TokenRespVO> refresh(@Valid @RequestBody RefreshReqVO req) {
@@ -4,8 +4,10 @@ package com.fruitshorts.app.controller;
import com.fruitshorts.commerce.dal.entity.Orders;
import com.fruitshorts.commerce.service.OrderService;
import com.fruitshorts.commerce.service.ProductService;
import com.fruitshorts.commerce.service.UnlockService;
import com.fruitshorts.commerce.service.WalletService;
import com.fruitshorts.commerce.vo.ProductRespVO;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
@@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
@@ -37,6 +40,7 @@ public class AppCommerceController {
private final WalletService walletService;
private final UnlockService unlockService;
private final OrderService orderService;
private final ProductService productService;
/** 我的钱包:余额 + 最近流水,客户端一处渲染。 */
@Operation(summary = "我的钱包(余额 + 最近流水)")
@@ -49,6 +53,22 @@ public class AppCommerceController {
return Result.ok(data);
}
/** 充值商品列表:只返回上架商品,type 可选(COIN_PACK/VIP)。 */
@Operation(summary = "充值商品列表(只返回上架商品,type 可选 COIN_PACK/VIP)")
@GetMapping("/products")
public Result<List<ProductRespVO>> products(@RequestParam(required = false) String type) {
return Result.ok(productService.listForApp(type));
}
/** 创建充值订单:body 传 productId,返回 CREATED 状态的订单,等待后续支付。 */
@Operation(summary = "创建充值订单(body 传 productId)")
@PostMapping("/orders")
public Result<Orders> createOrder(@RequestBody Map<String, Object> body) {
Object productId = body.get("productId");
Long pid = productId == null ? null : Long.valueOf(productId.toString());
return Result.ok(orderService.createOrder(SecurityUtils.getAppUserId(), pid));
}
/** 金币解锁分集:幂等,重复点击不会重复扣币。 */
@Operation(summary = "金币解锁分集(幂等,重复点击不重复扣币)")
@PostMapping("/episodes/{episodeId}/unlock")
@@ -0,0 +1,33 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller;
import com.fruitshorts.app.service.AppSearchService;
import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 客户端搜索:按标题或简介模糊匹配已上线短剧。 */
@Tag(name = "客户端-搜索")
@RestController
@RequestMapping("/app")
@RequiredArgsConstructor
public class AppSearchController {
private final AppSearchService appSearchService;
@Operation(summary = "搜索短剧(按标题/简介模糊匹配,只返回已上线)")
@GetMapping("/search")
public Result<PageResult<AppDramaCardVO>> search(@RequestParam String keyword,
@RequestParam(defaultValue = "1") int pageIndex,
@RequestParam(defaultValue = "20") int pageSize) {
return Result.ok(appSearchService.search(keyword, pageIndex, pageSize));
}
}
@@ -158,6 +158,7 @@ public class AppCatalogService {
card.setTotalEpisodes(drama.getTotalEpisodes());
card.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1);
card.setViewCount(drama.getViewCount());
card.setLanguage(drama.getLanguage());
card.setPreview(preview == null ? null : toEpisode(preview));
return card;
}
@@ -0,0 +1,44 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.content.enums.ContentStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 客户端搜索:只搜已上线短剧,命中标题或简介。
* 结果复用 {@link AppCatalogService#toCards} 的卡片转换,保证与首页卡片字段一致。
*/
@Service
@RequiredArgsConstructor
public class AppSearchService {
private static final int MAX_PAGE_SIZE = 50;
private final DramaMapper dramaMapper;
private final AppCatalogService appCatalogService;
public PageResult<AppDramaCardVO> search(String keyword, int pageIndex, int pageSize) {
if (!StringUtils.hasText(keyword)) {
return PageResult.empty();
}
int safeIndex = Math.max(pageIndex, 1);
int safeSize = Math.min(Math.max(pageSize, 1), MAX_PAGE_SIZE);
Page<Drama> page = new Page<>(safeIndex, safeSize);
Page<Drama> result = dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
.eq(Drama::getStatus, ContentStatus.PUBLISHED.getCode())
.and(w -> w.like(Drama::getTitle, keyword.trim())
.or().like(Drama::getSynopsis, keyword.trim()))
.orderByDesc(Drama::getSort)
.orderByDesc(Drama::getId));
return new PageResult<>(result.getTotal(), appCatalogService.toCards(result.getRecords()));
}
}
@@ -2,6 +2,7 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/** 列表卡片,用于首页/发现页/分类分区 */
@@ -17,6 +18,9 @@ public class AppDramaCardVO {
private Integer totalEpisodes;
private Boolean isHot;
private Long viewCount;
/** 语种,供客户端按语言筛选(如 zh-CN / id)。 */
@Schema(description = "短剧语种,如 zh-CN / id")
private String language;
/**
* 首个可播免费集。首页竖屏播放需要在列表阶段就能起播,
* 带上这一集可以省掉「点开→请求详情→再请求播放地址」的往返。
@@ -0,0 +1,14 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/** Google 登录请求。 */
@Data
public class GoogleLoginReqVO {
@NotBlank(message = "缺少 Google idToken")
private String idToken;
}