2026-05-23-assign-love-value-to-member.md 17 KB

管理员分配爱心值给家庭成员 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 管理员在家庭成员列表中,可从用户总账号余额分配爱心值给指定成员,同时写两条流水记录。

Architecture: 后端新增 assignLoveValue 接口,在事务中完成余额校验、双向扣增、流水写入;前端在 familyMember.vue 新增爱心值列、分配按钮和弹窗,通过新增 prop userLoveValuedetail.vue 获取总账号余额。

Tech Stack: Java 17、Spring Boot、MyBatis-Plus(后端);Vue 2.6、Element UI(前端)


文件变更清单

操作 文件
Modify happytree-sys/src/main/java/com/ringzle/modules/core/dto/FamilyMemberDTO.java
Modify happytree-sys/src/main/java/com/ringzle/modules/core/service/FamilyMemberService.java
Modify happytree-sys/src/main/java/com/ringzle/modules/core/service/impl/FamilyMemberServiceImpl.java
Modify happytree-admin/src/main/java/com/ringzle/modules/core/FamilyMemberController.java
Modify SxsnPC/src/views/modules/member/components/familyMember.vue
Modify SxsnPC/src/views/modules/member/detail.vue

Task 1: FamilyMemberDTO 新增 loveValue 字段

Files:

  • Modify: happytree-sys/src/main/java/com/ringzle/modules/core/dto/FamilyMemberDTO.java

  • [ ] Step 1: 在 channelIds 字段之前插入 loveValue 字段

找到文件中 channelIds 字段:

	@ApiModelProperty(value = "可用渠道ID,逗号分隔")
	private String channelIds;

在其前面插入:

	@ApiModelProperty(value = "爱心值")
	private BigDecimal loveValue;

同时确认文件顶部已有 import java.math.BigDecimal;(已有则跳过)。

  • Step 2: Commit
cd /workspace/sxsn/happytree
git add happytree-sys/src/main/java/com/ringzle/modules/core/dto/FamilyMemberDTO.java
git commit -m "feat: FamilyMemberDTO 新增 loveValue 字段"

Task 2: FamilyMemberService 新增 assignLoveValue 接口声明

Files:

  • Modify: happytree-sys/src/main/java/com/ringzle/modules/core/service/FamilyMemberService.java

  • [ ] Step 1: 在接口末尾新增方法声明

找到文件末尾 getMemberChannels 方法声明后,在 } 之前插入:

    /**
     * 管理员从总账号分配爱心值给家庭成员
     * @param memberId 家庭成员 ID
     * @param userId   总账号用户 ID
     * @param amount   分配数量,必须 > 0 且 <= 总账号余额
     */
    void assignLoveValue(Long memberId, Long userId, BigDecimal amount);

同时在文件顶部 import 区域添加(若缺少):

import java.math.BigDecimal;
  • Step 2: Commit
cd /workspace/sxsn/happytree
git add happytree-sys/src/main/java/com/ringzle/modules/core/service/FamilyMemberService.java
git commit -m "feat: FamilyMemberService 新增 assignLoveValue 接口"

Task 3: FamilyMemberServiceImpl — page() 填充 loveValue + 实现 assignLoveValue

Files:

  • Modify: happytree-sys/src/main/java/com/ringzle/modules/core/service/impl/FamilyMemberServiceImpl.java

背景:

  • FamilyMemberServiceImpl 已注入 appUserServiceAppUserService
  • FamilyMemberServiceImpl 已有 page() 重写方法,其中已通过 baseDao.selectBatchIds 批量查出 Entity 列表(用于填充 channelNames)
  • LoveValueRecordEntity 使用 Builder 模式,字段:userId, capitalFlow, transactionType, memberId, loveQuantity
  • transactionType=3 为本次新增类型(管理员分配)
  • capitalFlow=1 收入,capitalFlow=2 支出

Step 1: 在 page() 方法中复用 entities,填充 loveValue

找到 page() 方法中已有的 entities 批量查询和 channelNames 填充逻辑,在 list.forEach 填充 channelNames 的同时,追加填充 loveValue。

当前 page() 方法中有:

    List<FamilyMemberEntity> entities = baseDao.selectBatchIds(
        list.stream().map(FamilyMemberDTO::getId).collect(Collectors.toList())
    );

在该查询之后,建立 memberId -> loveValue 的 Map:

    Map<Long, BigDecimal> memberLoveValueMap = entities.stream()
        .collect(Collectors.toMap(FamilyMemberEntity::getId, e ->
            e.getLoveValue() != null ? e.getLoveValue() : BigDecimal.ZERO));

然后在 list.forEach 中,在 dto.setChannelNames(names) 之后追加:

        dto.setLoveValue(memberLoveValueMap.getOrDefault(dto.getId(), BigDecimal.ZERO));
  • Step 1: 修改 page() 方法,填充 loveValue

读取当前 page() 方法完整内容,按上述说明修改。完整修改后的 page() 方法应如下:

@Override
public PageData<FamilyMemberDTO> page(Map<String, Object> params) {
    PageData<FamilyMemberDTO> pageData = super.page(params);
    List<FamilyMemberDTO> list = pageData.getList();
    if (CollectionUtil.isEmpty(list)) {
        return pageData;
    }

    List<FamilyMemberEntity> entities = baseDao.selectBatchIds(
        list.stream().map(FamilyMemberDTO::getId).collect(Collectors.toList())
    );

    Map<Long, String> memberChannelIdsMap = list.stream()
        .filter(dto -> StringUtils.isNotBlank(dto.getChannelIds()))
        .collect(Collectors.toMap(FamilyMemberDTO::getId, FamilyMemberDTO::getChannelIds));

    Set<Long> channelIdSet = memberChannelIdsMap.values().stream()
        .flatMap(ids -> Arrays.stream(ids.split(",")))
        .map(String::trim)
        .filter(StringUtils::isNotBlank)
        .flatMap(id -> {
            try { return java.util.stream.Stream.of(Long.parseLong(id)); }
            catch (NumberFormatException e) { return java.util.stream.Stream.empty(); }
        })
        .collect(Collectors.toSet());

    Map<Long, String> channelNameMap = new HashMap<>();
    if (!channelIdSet.isEmpty()) {
        Collection<ChannelEntity> channels = channelDao.selectBatchIds(channelIdSet);
        channels.forEach(c -> channelNameMap.put(c.getId(), c.getChannelName()));
    }

    Map<Long, BigDecimal> memberLoveValueMap = entities.stream()
        .collect(Collectors.toMap(FamilyMemberEntity::getId, e ->
            e.getLoveValue() != null ? e.getLoveValue() : BigDecimal.ZERO));

    list.forEach(dto -> {
        String channelIds = memberChannelIdsMap.get(dto.getId());
        if (StringUtils.isBlank(channelIds)) {
            dto.setChannelNames("");
        } else {
            String names = Arrays.stream(channelIds.split(","))
                .map(String::trim)
                .filter(StringUtils::isNotBlank)
                .map(id -> {
                    try { return channelNameMap.getOrDefault(Long.parseLong(id), ""); }
                    catch (NumberFormatException e) { return ""; }
                })
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.joining("、"));
            dto.setChannelNames(names);
        }
        dto.setLoveValue(memberLoveValueMap.getOrDefault(dto.getId(), BigDecimal.ZERO));
    });

    return pageData;
}
  • Step 2: 新增 assignLoveValue() 方法

getMemberChannels() 方法之后,类的 } 之前插入:

@Override
@Transactional(rollbackFor = Exception.class)
public void assignLoveValue(Long memberId, Long userId, BigDecimal amount) {
    if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
        throw new RenException("分配数量必须大于0");
    }

    // 校验总账号余额
    AppUserEntity user = appUserService.getEntityById(userId);
    if (user == null) {
        throw new RenException("用户不存在");
    }
    if (user.getLoveValue() == null || user.getLoveValue().compareTo(amount) < 0) {
        throw new RenException("总账号余额不足");
    }

    // 校验成员属于该用户
    FamilyMemberEntity member = baseDao.selectById(memberId);
    if (member == null || !member.getUserId().equals(userId)) {
        throw new RenException("家庭成员不存在");
    }

    // 扣减总账号余额
    user.setLoveValue(user.getLoveValue().subtract(amount));
    appUserService.updateById(user);

    // 增加成员余额
    BigDecimal memberLove = member.getLoveValue() != null ? member.getLoveValue() : BigDecimal.ZERO;
    member.setLoveValue(memberLove.add(amount));
    baseDao.updateById(member);

    // 写流水:总账号支出
    loveValueRecordDao.insert(LoveValueRecordEntity.builder()
        .userId(userId)
        .capitalFlow(2)
        .transactionType(3)
        .memberId(memberId)
        .loveQuantity(amount.negate())
        .build());

    // 写流水:成员收入
    loveValueRecordDao.insert(LoveValueRecordEntity.builder()
        .userId(userId)
        .capitalFlow(1)
        .transactionType(3)
        .memberId(memberId)
        .loveQuantity(amount)
        .build());
}
  • Step 3: 注入 LoveValueRecordDao 和相关 import

FamilyMemberServiceImpl@Autowired 注入区域新增:

@Autowired
private com.ringzle.modules.core.dao.LoveValueRecordDao loveValueRecordDao;

在文件顶部 import 区域新增(若缺少):

import com.ringzle.modules.core.dao.LoveValueRecordDao;
import com.ringzle.modules.core.entity.LoveValueRecordEntity;
  • Step 4: 确认 appUserService.getEntityById() 方法存在

检查 AppUserService 接口是否有 getEntityById(Long id) 方法:

grep -n "getEntityById\|getById\|selectById" /workspace/sxsn/happytree/happytree-sys/src/main/java/com/ringzle/modules/core/service/AppUserService.java | head -10

若没有 getEntityById,改用 appUserService 的父类方法。检查 AppUserServiceImpl 中是否有直接查 Entity 的方法:

grep -n "getEntityById\|selectById\|AppUserEntity" /workspace/sxsn/happytree/happytree-sys/src/main/java/com/ringzle/modules/core/service/impl/AppUserServiceImpl.java | head -20

若无,则直接注入 AppUserDao 来查询 Entity:

@Autowired
private com.ringzle.modules.core.dao.AppUserDao appUserDao;

并将 assignLoveValue 中的 appUserService.getEntityById(userId) 替换为:

AppUserEntity user = appUserDao.selectById(userId);

appUserService.updateById(user) 替换为:

appUserDao.updateById(user);
  • Step 5: Commit
cd /workspace/sxsn/happytree
git add happytree-sys/src/main/java/com/ringzle/modules/core/service/impl/FamilyMemberServiceImpl.java
git commit -m "feat: page() 填充 loveValue,新增 assignLoveValue 实现"

Task 4: FamilyMemberController 新增接口

Files:

  • Modify: happytree-admin/src/main/java/com/ringzle/modules/core/FamilyMemberController.java

  • [ ] Step 1: 新增 assignLoveValue 接口

export 方法之后,类的 } 之前插入:

@PostMapping("assignLoveValue")
@ApiOperation("分配爱心值给家庭成员")
@LogOperation("分配爱心值")
public Result assignLoveValue(@RequestParam Long memberId,
                               @RequestParam Long userId,
                               @RequestParam java.math.BigDecimal amount) {
    familyMemberService.assignLoveValue(memberId, userId, amount);
    return new Result();
}
  • Step 2: 确认 import

文件顶部已有 import com.ringzle.common.utils.Result;import com.ringzle.common.annotation.LogOperation;,无需新增。

  • Step 3: Commit
cd /workspace/sxsn/happytree
git add happytree-admin/src/main/java/com/ringzle/modules/core/FamilyMemberController.java
git commit -m "feat: 新增 POST /core/family/member/assignLoveValue 接口"

Task 5: 前端 familyMember.vue — 爱心值列 + 分配按钮 + 弹窗

Files:

  • Modify: SxsnPC/src/views/modules/member/components/familyMember.vue

  • [ ] Step 1: 新增 userLoveValue prop

找到:

props: ['userId'],

替换为:

props: ['userId', 'userLoveValue'],
  • Step 2: 在「所属渠道」列后新增「爱心值」列

找到:

      <el-table-column label="所属渠道">
        <template slot-scope="scope">{{ scope.row.channelNames || '-' }}</template>
      </el-table-column>

在其后插入:

      <el-table-column label="爱心值" width="100">
        <template slot-scope="scope">{{ scope.row.loveValue || 0 }}</template>
      </el-table-column>
  • Step 3: 新增操作列(分配按钮)

在「参与活动次数」列之后、</el-table> 之前插入:

      <el-table-column label="操作" width="80">
        <template slot-scope="scope">
          <el-button type="text" @click="handleAssign(scope.row)">分配</el-button>
        </template>
      </el-table-column>
  • Step 4: 新增分配弹窗

</div> 最外层结束标签之前(<style> 之前)插入弹窗:

    <el-dialog width="400px" :visible.sync="assignShow" title="分配爱心值" @close="cancelAssign">
      <el-form label-width="120px" style="width: 90%; margin: 0 auto;">
        <el-form-item label="成员姓名">
          <span>{{ assignForm.memberName }}</span>
        </el-form-item>
        <el-form-item label="总账号余额">
          <span>{{ userLoveValue || 0 }}</span>
        </el-form-item>
        <el-form-item label="分配数量">
          <el-input-number
            v-model="assignForm.amount"
            :min="1"
            :max="userLoveValue || 0"
            :precision="0"
            style="width: 100%;"
          ></el-input-number>
        </el-form-item>
      </el-form>
      <div style="display: flex; justify-content: flex-end; margin-top: 20px; padding-right: 5%;">
        <el-button :loading="assignLoading" type="primary" @click="submitAssign">确 认</el-button>
        <el-button @click="cancelAssign">取 消</el-button>
      </div>
    </el-dialog>
  • Step 5: 在 data() 中新增弹窗相关状态

找到 data()return 对象,在 loading: false 后新增:

      assignShow: false,
      assignLoading: false,
      assignForm: {
        memberId: null,
        memberName: '',
        amount: 1
      },
  • Step 6: 在 methods 中新增 handleAssign、submitAssign、cancelAssign

pageCurrentChangeHandle 方法之后新增:

    handleAssign (row) {
      this.assignForm.memberId = row.id
      this.assignForm.memberName = row.name
      this.assignForm.amount = 1
      this.assignShow = true
    },
    submitAssign () {
      if (!this.assignForm.amount || this.assignForm.amount <= 0) {
        return this.$message.error('分配数量必须大于0')
      }
      this.assignLoading = true
      this.$http.post('/core/family/member/assignLoveValue', null, {
        params: {
          memberId: this.assignForm.memberId,
          userId: this.userId,
          amount: this.assignForm.amount
        }
      }).then(res => {
        if (res.data.code !== 0) {
          this.$message.error(res.data.msg)
        } else {
          this.$message.success('分配成功')
          this.cancelAssign()
          this.getList()
        }
      }).catch(() => {
        this.$message.error('操作失败,请重试')
      }).finally(() => {
        this.assignLoading = false
      })
    },
    cancelAssign () {
      this.assignShow = false
      this.assignForm = { memberId: null, memberName: '', amount: 1 }
    }
  • Step 7: Commit
cd /workspace/sxsn/SxsnPC
git add src/views/modules/member/components/familyMember.vue
git commit -m "feat: 家庭成员列表新增爱心值列和分配功能"

Task 6: detail.vue 传入 userLoveValue prop

Files:

  • Modify: SxsnPC/src/views/modules/member/detail.vue

  • [ ] Step 1: 找到 family-member 组件调用处

找到(约第 100 行):

<family-member v-else-if="tabIndex===3" :userId="userId"></family-member>

替换为:

<family-member v-else-if="tabIndex===3" :userId="userId" :userLoveValue="assetInfo ? assetInfo.loveValue : 0"></family-member>
  • Step 2: 手动验证

打开管理端会员详情页,切换到「家庭成员」tab:

  • 列表出现「爱心值」列,展示各成员余额
  • 每行有「分配」按钮
  • 点击「分配」弹窗显示成员姓名、总账号余额、分配数量输入框
  • 输入数量后点击确认,列表刷新,成员爱心值增加,总账号余额减少

  • [ ] Step 3: Commit

cd /workspace/sxsn/SxsnPC
git add src/views/modules/member/detail.vue
git commit -m "feat: detail.vue 向家庭成员组件传入总账号爱心值余额"

自检清单

  • 规格「分配数量 > 0」→ Task 3 Step 2 中 amount.compareTo(BigDecimal.ZERO) <= 0 校验
  • 规格「分配数量 ≤ 总账号余额」→ Task 3 Step 2 中 user.getLoveValue().compareTo(amount) < 0 校验
  • 规格「总账号 loveValue 扣减」→ Task 3 Step 2 subtract
  • 规格「成员 loveValue 增加」→ Task 3 Step 2 add
  • 规格「写两条流水,transactionType=3」→ Task 3 Step 2 两次 loveValueRecordDao.insert
  • 规格「总账号支出:capitalFlow=2,loveQuantity 为负」→ Task 3 Step 2 amount.negate()
  • 规格「成员收入:capitalFlow=1,loveQuantity 为正」→ Task 3 Step 2
  • 规格「前端展示爱心值列」→ Task 5 Step 2
  • 规格「前端分配按钮 + 弹窗」→ Task 5 Step 3-6
  • 规格「弹窗展示总账号余额」→ Task 5 Step 4 + Task 6
  • 规格「成功后刷新列表」→ Task 5 Step 6 getList()
  • Task 3 Step 4 处理了 appUserService.getEntityById 可能不存在的情况