|
|
@@ -5,9 +5,16 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.etotem.cfc.common.Result;
|
|
|
import com.etotem.cfc.dto.CfBalanceLogVO;
|
|
|
import com.etotem.cfc.dto.CfBalanceView;
|
|
|
+import com.etotem.cfc.dto.FamilyEarningsMemberDTO;
|
|
|
+import com.etotem.cfc.dto.FamilyEarningsSummaryDTO;
|
|
|
+import com.etotem.cfc.entity.CfBalance;
|
|
|
import com.etotem.cfc.entity.CfBalanceLog;
|
|
|
import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.CfBalanceLogMapper;
|
|
|
+import com.etotem.cfc.mapper.CfBalanceMapper;
|
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
import com.etotem.cfc.service.CfBalanceService;
|
|
|
import com.etotem.cfc.service.CouponService;
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
@@ -17,28 +24,41 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Calendar;
|
|
|
import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
* C 端 CF 值查询接口(只读)。
|
|
|
*
|
|
|
* <p>个人 CF 值账本为成员维度(family_members.id),本控制器把「登录 userId → memberId」
|
|
|
- * 的解析口径与 {@code CfEarnDispatchService}(F-3a)保持一致:按 user_id 在家庭内匹配,
|
|
|
- * 取最小 id 的一条(LIMIT 1);找不到对应家庭成员时返回明确错误码,不返回空数据。</p>
|
|
|
+ * 的解析口径与 {@code JwtInterceptor} 保持一致:优先使用拦截器按
|
|
|
+ * {@code users.family_id + family_members} 解析出的 {@code currentMemberId},
|
|
|
+ * 仅在缺失时回退到历史口径(user_id 最小一条)。</p>
|
|
|
*
|
|
|
- * <p>仅提供三个只读接口:余额快照 / 流水分页 / 家庭共享优惠券列表,不提供任何写接口。
|
|
|
- * 余额与流水走统一账本 {@link CfBalanceService};家庭券复用 {@link CouponService#listMyCoupons}
|
|
|
- * (N3 家庭化口径:family_coupon + coupon 联合,家庭内成员共享可见)。</p>
|
|
|
+ * <p>仅提供只读接口:余额快照 / 流水分页 / 周期统计 / 家庭 CF 总览 / 推广类 CF 收益 /
|
|
|
+ * 家庭共享优惠券列表。余额与流水走统一账本 {@link CfBalanceService};
|
|
|
+ * 家庭券复用 {@link CouponService#listMyCoupons}(N3 家庭化口径:family_coupon + coupon 联合)。</p>
|
|
|
*
|
|
|
- * <p>接口约定见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §七 F-7a。</p>
|
|
|
+ * <p>接口约定见 docs/superpowers/specs/2026-10-25-cf-value-single-source-of-truth.md §5/§6。</p>
|
|
|
*/
|
|
|
@RestController
|
|
|
@RequestMapping("/api/cf")
|
|
|
public class CfBalanceController {
|
|
|
|
|
|
+ /** 推广类 CF 收益:推荐注册 F24 + 直接销售奖 F03a + 间接销售奖 F03b + 邀请里程碑 F11 */
|
|
|
+ private static final Set<String> PROMOTION_REF_TYPES = new HashSet<>(Arrays.asList(
|
|
|
+ "referral_register", "F03a", "F03b", "invite_milestone"));
|
|
|
+
|
|
|
@Resource
|
|
|
private CfBalanceService cfBalanceService;
|
|
|
|
|
|
@@ -48,12 +68,22 @@ public class CfBalanceController {
|
|
|
@Resource
|
|
|
private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private CfBalanceMapper cfBalanceMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private CfBalanceLogMapper cfBalanceLogMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
/**
|
|
|
* 我的 CF 值快照:可用余额 / 冻结 / 累计获得 / 已提现 / 已兑换消费。
|
|
|
*/
|
|
|
@GetMapping("/balance")
|
|
|
- public Result<CfBalanceView> balance(@RequestAttribute("userId") Long userId) {
|
|
|
- Long memberId = resolveMemberId(userId);
|
|
|
+ public Result<CfBalanceView> balance(@RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
|
|
|
+ Long memberId = resolveMemberId(userId, currentMemberId);
|
|
|
if (memberId == null) {
|
|
|
return Result.error(404, "未找到当前用户对应的家庭成员,无法查询 CF 值");
|
|
|
}
|
|
|
@@ -65,9 +95,10 @@ public class CfBalanceController {
|
|
|
*/
|
|
|
@GetMapping("/logs")
|
|
|
public Result<Page<CfBalanceLogVO>> logs(@RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
|
|
|
@RequestParam(defaultValue = "1") Integer page,
|
|
|
@RequestParam(defaultValue = "20") Integer size) {
|
|
|
- Long memberId = resolveMemberId(userId);
|
|
|
+ Long memberId = resolveMemberId(userId, currentMemberId);
|
|
|
if (memberId == null) {
|
|
|
return Result.error(404, "未找到当前用户对应的家庭成员,无法查询 CF 流水");
|
|
|
}
|
|
|
@@ -80,6 +111,151 @@ public class CfBalanceController {
|
|
|
return Result.success(vo);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 我的 CF 周期统计:今日 / 本月获得 + 累计获得(cf_balance.total_earned)。
|
|
|
+ */
|
|
|
+ @GetMapping("/stats")
|
|
|
+ public Result<Map<String, Object>> stats(@RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
|
|
|
+ Long memberId = resolveMemberId(userId, currentMemberId);
|
|
|
+ if (memberId == null) {
|
|
|
+ return Result.error(404, "未找到当前用户对应的家庭成员,无法查询 CF 统计");
|
|
|
+ }
|
|
|
+ CfBalanceView view = cfBalanceService.getBalance(memberId);
|
|
|
+
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
+ cal.set(Calendar.MINUTE, 0);
|
|
|
+ cal.set(Calendar.SECOND, 0);
|
|
|
+ cal.set(Calendar.MILLISECOND, 0);
|
|
|
+ Date startOfDay = cal.getTime();
|
|
|
+ cal.set(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ Date startOfMonth = cal.getTime();
|
|
|
+
|
|
|
+ List<CfBalanceLog> monthLogs = cfBalanceLogMapper.selectList(new LambdaQueryWrapper<CfBalanceLog>()
|
|
|
+ .eq(CfBalanceLog::getMemberId, memberId)
|
|
|
+ .eq(CfBalanceLog::getType, "earn")
|
|
|
+ .ge(CfBalanceLog::getCreatedAt, startOfMonth));
|
|
|
+ int monthEarned = 0;
|
|
|
+ int todayEarned = 0;
|
|
|
+ for (CfBalanceLog log : monthLogs) {
|
|
|
+ int amount = log.getAmount() == null ? 0 : log.getAmount();
|
|
|
+ monthEarned += amount;
|
|
|
+ if (log.getCreatedAt() != null && !log.getCreatedAt().before(startOfDay)) {
|
|
|
+ todayEarned += amount;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("todayEarned", todayEarned);
|
|
|
+ data.put("monthEarned", monthEarned);
|
|
|
+ data.put("totalEarned", view.getTotalEarned() == null ? 0 : view.getTotalEarned());
|
|
|
+ data.put("available", view.getAvailable() == null ? 0 : view.getAvailable());
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 家庭 CF 总览(成员维度):全家累计 / 可用 / 已提现 / 已兑换 + 成员贡献榜。
|
|
|
+ *
|
|
|
+ * <p>替代已停写的 {@code family_earnings} 旧账本,数据源为 {@code cf_balance}。</p>
|
|
|
+ */
|
|
|
+ @GetMapping("/family-summary")
|
|
|
+ public Result<FamilyEarningsSummaryDTO> familySummary(@RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
|
|
|
+ Long memberId = resolveMemberId(userId, currentMemberId);
|
|
|
+ if (memberId == null) {
|
|
|
+ return Result.error(404, "未找到当前用户对应的家庭成员,无法查询家庭 CF 总览");
|
|
|
+ }
|
|
|
+ FamilyMember self = familyMemberMapper.selectById(memberId);
|
|
|
+ Long familyId = self == null ? null : self.getFamilyId();
|
|
|
+
|
|
|
+ FamilyEarningsSummaryDTO summary = new FamilyEarningsSummaryDTO();
|
|
|
+ summary.setFamilyId(familyId);
|
|
|
+
|
|
|
+ List<CfBalance> balances = familyId == null ? Collections.emptyList()
|
|
|
+ : cfBalanceMapper.selectList(new LambdaQueryWrapper<CfBalance>()
|
|
|
+ .eq(CfBalance::getFamilyId, familyId));
|
|
|
+
|
|
|
+ List<Long> memberIds = balances.stream()
|
|
|
+ .map(CfBalance::getMemberId).filter(id -> id != null)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ Map<Long, FamilyMember> memberMap = memberIds.isEmpty() ? Collections.emptyMap()
|
|
|
+ : familyMemberMapper.selectBatchIds(memberIds).stream()
|
|
|
+ .collect(Collectors.toMap(FamilyMember::getId, m -> m, (a, b) -> a));
|
|
|
+
|
|
|
+ List<Long> userIds = balances.stream()
|
|
|
+ .map(CfBalance::getUserId).filter(id -> id != null)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ Map<Long, User> userMap = userIds.isEmpty() ? Collections.emptyMap()
|
|
|
+ : userMapper.selectBatchIds(userIds).stream()
|
|
|
+ .collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));
|
|
|
+
|
|
|
+ long totalEarned = 0L;
|
|
|
+ long available = 0L;
|
|
|
+ long withdrawn = 0L;
|
|
|
+ long exchanged = 0L;
|
|
|
+ List<FamilyEarningsMemberDTO> members = new ArrayList<>();
|
|
|
+ for (CfBalance balance : balances) {
|
|
|
+ long memberTotal = balance.getTotalEarned() == null ? 0L : balance.getTotalEarned();
|
|
|
+ long memberAvailable = balance.getAvailable() == null ? 0L : balance.getAvailable();
|
|
|
+ FamilyMember member = memberMap.get(balance.getMemberId());
|
|
|
+ User user = balance.getUserId() == null ? null : userMap.get(balance.getUserId());
|
|
|
+
|
|
|
+ FamilyEarningsMemberDTO dto = new FamilyEarningsMemberDTO();
|
|
|
+ dto.setUserId(balance.getUserId());
|
|
|
+ dto.setNickname(user != null && user.getNickname() != null ? user.getNickname()
|
|
|
+ : (member == null ? null : member.getNickname()));
|
|
|
+ dto.setAvatar(user != null && user.getAvatar() != null ? user.getAvatar()
|
|
|
+ : (member == null ? null : member.getAvatar()));
|
|
|
+ dto.setAllocatedAmount(memberTotal);
|
|
|
+ dto.setAvailableAmount(memberAvailable);
|
|
|
+ dto.setConsumedAmount(Math.max(0L, memberTotal - memberAvailable));
|
|
|
+ dto.setIsAdmin(user != null && Boolean.TRUE.equals(user.getIsFamilyAdmin()));
|
|
|
+ // 新模型无家庭公共池,字段保留兼容前端;家庭内共享通过优惠券 / 成员转让实现
|
|
|
+ dto.setCanUseFamilyBalance(true);
|
|
|
+ members.add(dto);
|
|
|
+
|
|
|
+ totalEarned += memberTotal;
|
|
|
+ available += memberAvailable;
|
|
|
+ withdrawn += balance.getWithdrawn() == null ? 0L : balance.getWithdrawn();
|
|
|
+ exchanged += balance.getExchanged() == null ? 0L : balance.getExchanged();
|
|
|
+ }
|
|
|
+ members.sort((a, b) -> Long.compare(
|
|
|
+ b.getAllocatedAmount() == null ? 0L : b.getAllocatedAmount(),
|
|
|
+ a.getAllocatedAmount() == null ? 0L : a.getAllocatedAmount()));
|
|
|
+
|
|
|
+ summary.setTotalEarned(totalEarned);
|
|
|
+ summary.setAvailableAmount(available);
|
|
|
+ summary.setWithdrawnAmount(withdrawn);
|
|
|
+ summary.setDistributedAmount(exchanged);
|
|
|
+ summary.setMembers(members);
|
|
|
+ return Result.success(summary);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 推广类 CF 收益:F24 推荐注册 + F03a 直接销售奖 + F03b 间接销售奖 + F11 邀请里程碑。
|
|
|
+ *
|
|
|
+ * <p>仅统计本人当前成员的 {@code cf_balance_log}(type=earn)。</p>
|
|
|
+ */
|
|
|
+ @GetMapping("/promotion-earnings")
|
|
|
+ public Result<Map<String, Object>> promotionEarnings(@RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
|
|
|
+ Long memberId = resolveMemberId(userId, currentMemberId);
|
|
|
+ if (memberId == null) {
|
|
|
+ return Result.error(404, "未找到当前用户对应的家庭成员,无法查询推广收益");
|
|
|
+ }
|
|
|
+ List<CfBalanceLog> logs = cfBalanceLogMapper.selectList(new LambdaQueryWrapper<CfBalanceLog>()
|
|
|
+ .eq(CfBalanceLog::getMemberId, memberId)
|
|
|
+ .eq(CfBalanceLog::getType, "earn")
|
|
|
+ .in(CfBalanceLog::getRefType, PROMOTION_REF_TYPES));
|
|
|
+ int total = logs.stream().mapToInt(log -> log.getAmount() == null ? 0 : log.getAmount()).sum();
|
|
|
+
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("totalEarned", total);
|
|
|
+ data.put("refTypes", new ArrayList<>(PROMOTION_REF_TYPES));
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 家庭共享优惠券列表(N3 口径):按登录用户所属家庭查券实例 + 券模板,
|
|
|
* 返回券名 / 状态 / 有效期 / 抵扣面额等展示字段,家庭内成员共享可见。
|
|
|
@@ -102,10 +278,18 @@ public class CfBalanceController {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 登录 userId → 家庭成员(family_members.id)。
|
|
|
- * 口径与 {@code CfEarnDispatchService} 的 resolveMemberId 一致:user_id 匹配 +
|
|
|
- * 按 id 升序取第一条(LIMIT 1),未命中返回 null。
|
|
|
+ * 登录 userId → 当前家庭成员(family_members.id)。
|
|
|
+ *
|
|
|
+ * <p>优先使用 {@code JwtInterceptor} 按 {@code users.family_id} 解析出的 currentMemberId;
|
|
|
+ * 缺失时回退到历史口径(user_id 匹配 + id 升序 LIMIT 1),保证无家庭用户兼容。</p>
|
|
|
*/
|
|
|
+ private Long resolveMemberId(Long userId, Long currentMemberId) {
|
|
|
+ if (currentMemberId != null && currentMemberId > 0) {
|
|
|
+ return currentMemberId;
|
|
|
+ }
|
|
|
+ return resolveMemberId(userId);
|
|
|
+ }
|
|
|
+
|
|
|
private Long resolveMemberId(Long userId) {
|
|
|
if (userId == null || userId <= 0) {
|
|
|
return null;
|
|
|
@@ -116,4 +300,4 @@ public class CfBalanceController {
|
|
|
.last("LIMIT 1"));
|
|
|
return member == null ? null : member.getId();
|
|
|
}
|
|
|
-}
|
|
|
+}
|