|
|
@@ -0,0 +1,362 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.task.TaskController;
|
|
|
+import com.etotem.cfc.dto.CompleteTaskDTO;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.PointsLog;
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
+import com.etotem.cfc.entity.TaskExecution;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.PointsLogMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskExecutionMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
+import com.etotem.cfc.service.CfPathwayEarnService;
|
|
|
+import com.etotem.cfc.service.EnergyService;
|
|
|
+import com.etotem.cfc.service.MembershipService;
|
|
|
+import com.etotem.cfc.service.NotificationDispatchService;
|
|
|
+import com.etotem.cfc.service.ProblemCompletionService;
|
|
|
+import com.etotem.cfc.service.ProfileComputeService;
|
|
|
+import com.etotem.cfc.service.StreakService;
|
|
|
+import com.etotem.cfc.service.TaskReminderConfigService;
|
|
|
+import com.etotem.cfc.service.TaskService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyInt;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 任务完成打卡(能力键 {@code task.task.complete})契约级单测(SC-C-02 / TC-C-02 / TC-C-02-E / TC-C-02-P)。
|
|
|
+ *
|
|
|
+ * <p><b>契约锚点</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>需求:docs/tdd/SRS.md «三、未覆盖控制器» TaskController(18 方法,0 覆盖)</li>
|
|
|
+ * <li>设计:TaskController.completeTask {@code @PostMapping("/{id}/complete")} + TaskService.completeTask</li>
|
|
|
+ * <li>契约:docs/superpowers/api/API_REFERENCE.md §4.3 / §4.61.2 — {@code POST /api/tasks/{id}/complete},
|
|
|
+ * 请求 {@code CompleteTaskDTO{memberId?, photoUrl, content, contentType}},响应 {@code Result<Map>}(奖励/状态),打卡提交</li>
|
|
|
+ * <li>AC 条款:tests/test-cases/child.md TC-C-02(正常流:提交完成→待审核/写记录/重复提交不重复计分)、
|
|
|
+ * TC-C-02-E(异常流:重复/超期/驳回重做)、TC-C-02-P(权限流:非本人不越权)</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯 Mockito + 反射(无 Spring 上下文 / 无 DB / 无外部服务),
|
|
|
+ * 直接驱动 {@link TaskService#completeTask} 与 {@link TaskController#completeTask},
|
|
|
+ * 风格对齐同批任务测试(FamilyJoinValidationRulesContractTest / TaskReviewScT06ContractTest)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期(TDD-RED)</b>:本键为契约新增能力,src/main 现状核实:
|
|
|
+ * 端点/正常流/幂等/越权不发奖等大部分契约行为已落地(对应用例为绿);
|
|
|
+ * 但异常流 AC 条款「超期任务仍提交 → 被拒」(TC-C-02-E)在 {@code completeTask} 中
|
|
|
+ * 无 deadline 守卫:任务截止时间已过仍走「迟到扣分」完成链路并落库发奖。
|
|
|
+ * 因此「超期提交必须被拒(不完成、不发奖)」用例断言失败(红),
|
|
|
+ * GREEN 阶段需在 {@code completeTask} 补服务端超期守卫后转绿。</p>
|
|
|
+ *
|
|
|
+ * <p>「并发双点幂等仅计 1 次」以服务层顺序重复提交等价验证(同一任务两次调用仅 1 条积分流水)、
|
|
|
+ * 与 T24-A 幂等分支(已完成/待审核返回 alreadyCompleted 不再发奖)实现一致。</p>
|
|
|
+ */
|
|
|
+class TaskCompleteScC02ContractTest {
|
|
|
+
|
|
|
+ private TaskService service;
|
|
|
+ private TaskController controller;
|
|
|
+ private TaskService taskServiceMock; // controller 层转发的 mock(与 service 层真实实例隔离)
|
|
|
+
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ private PointsLogMapper pointsLogMapper;
|
|
|
+ private TaskExecutionMapper taskExecutionMapper;
|
|
|
+ private StreakService streakService;
|
|
|
+ private MembershipService membershipService;
|
|
|
+ private ProblemCompletionService problemCompletionService;
|
|
|
+ private ProfileComputeService profileComputeService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ // ============ service 层:真实 TaskService + mock 依赖 ============
|
|
|
+ service = new TaskService();
|
|
|
+ taskMapper = mock(TaskMapper.class);
|
|
|
+ familyMemberMapper = mock(FamilyMemberMapper.class);
|
|
|
+ pointsLogMapper = mock(PointsLogMapper.class);
|
|
|
+ taskExecutionMapper = mock(TaskExecutionMapper.class);
|
|
|
+ streakService = mock(StreakService.class);
|
|
|
+ EnergyService energyService = mock(EnergyService.class);
|
|
|
+ CfPathwayEarnService cfPathwayEarnService = mock(CfPathwayEarnService.class);
|
|
|
+ TaskReminderConfigService taskReminderConfigService = mock(TaskReminderConfigService.class);
|
|
|
+ NotificationDispatchService notificationDispatchService = mock(NotificationDispatchService.class);
|
|
|
+
|
|
|
+ inject(service, "taskMapper", taskMapper);
|
|
|
+ inject(service, "familyMemberMapper", familyMemberMapper);
|
|
|
+ inject(service, "pointsLogMapper", pointsLogMapper);
|
|
|
+ inject(service, "taskExecutionMapper", taskExecutionMapper);
|
|
|
+ inject(service, "streakService", streakService);
|
|
|
+ inject(service, "energyService", energyService);
|
|
|
+ inject(service, "cfPathwayEarnService", cfPathwayEarnService);
|
|
|
+ inject(service, "taskReminderConfigService", taskReminderConfigService);
|
|
|
+ inject(service, "notificationDispatchService", notificationDispatchService);
|
|
|
+
|
|
|
+ when(taskReminderConfigService.loadAllConfigs()).thenReturn(new HashMap<>());
|
|
|
+ when(streakService.checkAndAwardMilestone(anyLong(), anyInt())).thenReturn(0);
|
|
|
+
|
|
|
+ // ============ controller 层:真实 TaskController + mock 依赖 ============
|
|
|
+ controller = new TaskController();
|
|
|
+ taskServiceMock = mock(TaskService.class);
|
|
|
+ membershipService = mock(MembershipService.class);
|
|
|
+ problemCompletionService = mock(ProblemCompletionService.class);
|
|
|
+ profileComputeService = mock(ProfileComputeService.class);
|
|
|
+ inject(controller, "taskService", taskServiceMock);
|
|
|
+ inject(controller, "taskMapper", taskMapper);
|
|
|
+ inject(controller, "membershipService", membershipService);
|
|
|
+ inject(controller, "problemCompletionService", problemCompletionService);
|
|
|
+ inject(controller, "profileComputeService", profileComputeService);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 正常流(TC-C-02) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:需审核任务提交打卡 → 状态待家长审核、写入执行记录、不发积分")
|
|
|
+ void c02_正常流_需审核任务_提交进入待审核() {
|
|
|
+ Task t = task(11L, 100L, "pending", 1, new Date(System.currentTimeMillis() + 3_600_000L));
|
|
|
+ when(taskMapper.selectById(11L)).thenReturn(t);
|
|
|
+ when(familyMemberMapper.selectById(100L)).thenReturn(member(100L, 50, 0));
|
|
|
+
|
|
|
+ Map<String, Object> result = service.completeTask(11L, 100L, null, "完成睡前阅读", "text");
|
|
|
+
|
|
|
+ assertEquals(Boolean.TRUE, result.get("needReview"), "需审核任务应返回 needReview=true(待家长审核)");
|
|
|
+ assertEquals(0, result.get("pointsEarned"), "审核未通过前不应发积分");
|
|
|
+ assertEquals("pending_review", t.getStatus(), "状态应流转到 pending_review");
|
|
|
+ assertTrue(t.getCompletedAt() != null, "应写入完成时间(供家长审核)");
|
|
|
+ verify(pointsLogMapper, never()).insert(any(PointsLog.class));
|
|
|
+ verify(taskExecutionMapper).insert(any(TaskExecution.class)); // 完成动作写入记录
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:免审核任务按时完成 → 发积分(提前完成+1)、积分流水 1 条")
|
|
|
+ void c02_正常流_免审核任务_完成发积分() {
|
|
|
+ Task t = task(12L, 100L, "pending", 0, new Date(System.currentTimeMillis() + 3_600_000L));
|
|
|
+ when(taskMapper.selectById(12L)).thenReturn(t);
|
|
|
+ FamilyMember child = member(100L, 20, 0);
|
|
|
+ when(familyMemberMapper.selectById(100L)).thenReturn(child);
|
|
|
+
|
|
|
+ Map<String, Object> result = service.completeTask(12L, 100L, null, "读书打卡", "text");
|
|
|
+
|
|
|
+ // 提前完成:10 分 + 提前奖励 1 分;newBalance = 20 + 11
|
|
|
+ assertEquals(11, result.get("pointsEarned"), "提前完成应得 points+EARLY_BONUS(1)");
|
|
|
+ assertEquals(31, result.get("newBalance"), "完成后余额应更新");
|
|
|
+ assertEquals(Boolean.FALSE, result.get("needReview"));
|
|
|
+ assertEquals("completed", t.getStatus(), "免审核任务应直接完成");
|
|
|
+
|
|
|
+ ArgumentCaptor<PointsLog> cap = ArgumentCaptor.forClass(PointsLog.class);
|
|
|
+ verify(pointsLogMapper, times(1)).insert(cap.capture());
|
|
|
+ assertEquals(11, cap.getValue().getAmount(), "积分流水金额应与发放一致");
|
|
|
+ assertEquals(100L, cap.getValue().getMemberId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:重复提交(已完成任务再提交)→ 幂等成功、不重复计分不发奖")
|
|
|
+ void c02_正常流_重复提交_幂等不重复计分() {
|
|
|
+ Task t = task(13L, 100L, "completed", 0, new Date(System.currentTimeMillis() + 3_600_000L));
|
|
|
+ when(taskMapper.selectById(13L)).thenReturn(t);
|
|
|
+ when(familyMemberMapper.selectById(100L)).thenReturn(member(100L, 30, 0));
|
|
|
+
|
|
|
+ Map<String, Object> result = service.completeTask(13L, 100L, null, null, null);
|
|
|
+
|
|
|
+ assertEquals(Boolean.TRUE, result.get("alreadyCompleted"), "重复提交应幂等返回已完成标识");
|
|
|
+ assertEquals(0, result.get("pointsEarned"), "重复提交不应再计分");
|
|
|
+ verify(taskMapper, never()).updateById(any(Task.class));
|
|
|
+ verify(pointsLogMapper, never()).insert(any(PointsLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-0:同一任务连续提交两次「完成」→ 仅 1 条积分流水(TC-C-02-E 重复/并发双点等价验证)")
|
|
|
+ void c02_异常流_同任务连点两次_仅1条积分流水() {
|
|
|
+ Task t = task(14L, 100L, "pending", 0, new Date(System.currentTimeMillis() + 3_600_000L));
|
|
|
+ when(taskMapper.selectById(14L)).thenReturn(t);
|
|
|
+ when(familyMemberMapper.selectById(100L)).thenReturn(member(100L, 20, 0));
|
|
|
+
|
|
|
+ service.completeTask(14L, 100L, null, "打卡", "text");
|
|
|
+ service.completeTask(14L, 100L, null, "打卡", "text");
|
|
|
+
|
|
|
+ verify(pointsLogMapper, times(1)).insert(any(PointsLog.class));
|
|
|
+ assertEquals("completed", t.getStatus(), "最终状态应为 completed(不重复跃迁)");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 异常流(TC-C-02-E) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-1:超期任务仍提交应被拒(RED:deadline 守卫缺失)")
|
|
|
+ void c02e_异常流_超期任务提交_应被拒() {
|
|
|
+ // 截止时间已过 1 小时,任务仍为 pending
|
|
|
+ Task t = task(99L, 100L, "pending", 0, new Date(System.currentTimeMillis() - 3_600_000L));
|
|
|
+ when(taskMapper.selectById(99L)).thenReturn(t);
|
|
|
+ when(familyMemberMapper.selectById(100L)).thenReturn(member(100L, 20, 0));
|
|
|
+
|
|
|
+ // 契约(TC-C-02-E):超期任务仍提交 → 被拒(不允许完成、不得发奖)。
|
|
|
+ // 现状:无 deadline 守卫,走"迟到扣分"完成链路 → 断言失败(红)。
|
|
|
+ assertThrows(IllegalArgumentException.class,
|
|
|
+ () -> service.completeTask(99L, 100L, null, "逾期补交", "text"),
|
|
|
+ "[task.task.complete] 超期任务提交必须被拒(TC-C-02-E):当前实现无超期守卫,"
|
|
|
+ + "仍在完成并发奖。GREEN 阶段应在 completeTask 补 deadline 拦截。");
|
|
|
+ verify(taskMapper, never()).updateById(any(Task.class));
|
|
|
+ verify(pointsLogMapper, never()).insert(any(PointsLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-2:任务不存在 → 返回 null(上游转错误提示,杜绝静默成功)")
|
|
|
+ void c02_异常流_任务不存在_返回null() {
|
|
|
+ when(taskMapper.selectById(999L)).thenReturn(null);
|
|
|
+ assertNull(service.completeTask(999L, 100L, null, null, null));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 权限流(TC-C-02-P) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-1:非本人成员提交 → 不发奖、不修改任务状态")
|
|
|
+ void c02_权限流_非本人成员提交_不发奖不改状态() {
|
|
|
+ Task t = task(15L, 100L, "pending", 0, new Date(System.currentTimeMillis() + 3_600_000L)); // 归属成员 100
|
|
|
+ when(taskMapper.selectById(15L)).thenReturn(t);
|
|
|
+ when(familyMemberMapper.selectById(999L)).thenReturn(member(999L, 5, 0)); // 越权成员 999
|
|
|
+
|
|
|
+ Map<String, Object> result = service.completeTask(15L, 999L, null, "替别人打卡", "text");
|
|
|
+
|
|
|
+ assertEquals(Boolean.TRUE, result.get("alreadyCompleted"), "越权提交应幂等空结果");
|
|
|
+ assertEquals(0, result.get("pointsEarned"), "越权提交不得发奖");
|
|
|
+ assertEquals("pending", t.getStatus(), "任务状态不得被非本人提交修改");
|
|
|
+ verify(taskMapper, never()).updateById(any(Task.class));
|
|
|
+ verify(pointsLogMapper, never()).insert(any(PointsLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-2:会员任务非会员提交 → 拒绝(提示开通会员),不触达 Service")
|
|
|
+ void c02_权限流_会员任务非会员_拒绝() {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(55L);
|
|
|
+ t.setFamilyId(7L);
|
|
|
+ t.setMemberOnly(1);
|
|
|
+ when(taskMapper.selectById(55L)).thenReturn(t);
|
|
|
+ when(membershipService.getCurrentLevel(7L)).thenReturn(null); // 非会员
|
|
|
+
|
|
|
+ CompleteTaskDTO dto = new CompleteTaskDTO();
|
|
|
+ dto.setMemberId(100L);
|
|
|
+ Result<Map<String, Object>> result = controller.completeTask(55L, dto, 200L);
|
|
|
+
|
|
|
+ assertEquals(Integer.valueOf(500), result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("会员"), "应提示开通会员,实际: " + result.getMessage());
|
|
|
+ verify(taskServiceMock, never()).completeTask(any(), any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 控制器传输契约(API_REFERENCE §4.61.2) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("控制器契约-1:DTO{memberId,photoUrl,content,contentType} 原样转发 Service,响应 Result.success(Map)")
|
|
|
+ void controller_契约_dto字段转发与成功响应() {
|
|
|
+ stubNonMemberOnlyTask();
|
|
|
+ when(taskServiceMock.completeTask(60L, 100L, "http://img/a.png", "打卡内容", "text"))
|
|
|
+ .thenReturn(rewardMap(5, 50, false));
|
|
|
+
|
|
|
+ CompleteTaskDTO dto = new CompleteTaskDTO();
|
|
|
+ dto.setMemberId(100L);
|
|
|
+ dto.setPhotoUrl("http://img/a.png");
|
|
|
+ dto.setContent("打卡内容");
|
|
|
+ dto.setContentType("text");
|
|
|
+ Result<Map<String, Object>> result = controller.completeTask(60L, dto, 200L);
|
|
|
+
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode());
|
|
|
+ assertEquals("success", result.getMessage());
|
|
|
+ assertEquals(5, result.getData().get("pointsEarned"));
|
|
|
+ verify(taskServiceMock).completeTask(60L, 100L, "http://img/a.png", "打卡内容", "text");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("控制器契约-2:DTO 缺省 memberId → 回落 currentMemberId 转发 Service")
|
|
|
+ void controller_契约_memberId缺省回落当前成员() {
|
|
|
+ stubNonMemberOnlyTask();
|
|
|
+ when(taskServiceMock.completeTask(61L, 200L, null, "内容", null)).thenReturn(rewardMap(0, 30, false));
|
|
|
+
|
|
|
+ CompleteTaskDTO dto = new CompleteTaskDTO();
|
|
|
+ dto.setContent("内容");
|
|
|
+ Result<Map<String, Object>> result = controller.completeTask(61L, dto, 200L);
|
|
|
+
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode());
|
|
|
+ verify(taskServiceMock).completeTask(61L, 200L, null, "内容", null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("控制器契约-3:Service 返回 null → Result.error 明确提示(杜绝静默失败)")
|
|
|
+ void controller_契约_service返回null_错误提示() {
|
|
|
+ stubNonMemberOnlyTask();
|
|
|
+ when(taskServiceMock.completeTask(62L, 100L, null, null, null)).thenReturn(null);
|
|
|
+
|
|
|
+ CompleteTaskDTO dto = new CompleteTaskDTO();
|
|
|
+ dto.setMemberId(100L);
|
|
|
+ Result<Map<String, Object>> result = controller.completeTask(62L, dto, 200L);
|
|
|
+
|
|
|
+ assertEquals(Integer.valueOf(500), result.getCode());
|
|
|
+ assertEquals("任务不存在、无权完成或状态已变更", result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 辅助构造 ====================
|
|
|
+
|
|
|
+ private Task task(Long id, Long memberId, String status, Integer needReview, Date deadline) {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(id);
|
|
|
+ t.setFamilyId(1L);
|
|
|
+ t.setFamilyMemberId(memberId);
|
|
|
+ t.setTitle("睡前阅读");
|
|
|
+ t.setPoints(10);
|
|
|
+ t.setStatus(status);
|
|
|
+ t.setNeedReview(needReview);
|
|
|
+ t.setDeadline(deadline);
|
|
|
+ t.setCategory("学习类");
|
|
|
+ t.setCreatedAt(new Date());
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FamilyMember member(Long id, Integer totalPoints, Integer penaltyEnabled) {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ m.setId(id);
|
|
|
+ m.setUserId(1L);
|
|
|
+ m.setTotalPoints(totalPoints);
|
|
|
+ m.setPenaltyEnabled(penaltyEnabled);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> rewardMap(int points, int balance, boolean needReview) {
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
+ m.put("pointsEarned", points);
|
|
|
+ m.put("newBalance", balance);
|
|
|
+ m.put("needReview", needReview);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 让门禁(checkMemberOnly)放行:返回非会员任务的 stub(memberOnly != 1) */
|
|
|
+ private void stubNonMemberOnlyTask() {
|
|
|
+ when(taskMapper.selectById(anyLong())).thenAnswer(inv -> {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(inv.getArgument(0));
|
|
|
+ t.setFamilyId(1L);
|
|
|
+ t.setMemberOnly(0);
|
|
|
+ return t;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void inject(Object target, String field, Object value) throws Exception {
|
|
|
+ Field f = target.getClass().getDeclaredField(field);
|
|
|
+ f.setAccessible(true);
|
|
|
+ f.set(target, value);
|
|
|
+ }
|
|
|
+}
|