feat(commerce): 完成奖励活动管理接口

This commit is contained in:
Zhou Weidong committed 2026-11-13 13:43:16 +08:00
1 parent 32cac38a0e
commit 12c28963f5
10 files changed
+408 -3

No files matched your search

@@ -0,0 +1,136 @@
// Author: fruit.com.cn
package com.fruitshorts.commerce.reward.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.commerce.reward.dal.entity.RewardActivity;
import com.fruitshorts.commerce.reward.dal.mapper.RewardActivityMapper;
import com.fruitshorts.commerce.reward.vo.RewardActivityQueryReqVO;
import com.fruitshorts.commerce.reward.vo.RewardActivityRespVO;
import com.fruitshorts.commerce.reward.vo.RewardActivitySaveReqVO;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.common.pojo.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
/** 奖励中心活动的后台配置服务。 */
@Service
@RequiredArgsConstructor
public class RewardActivityAdminService {
private final RewardActivityMapper activityMapper;
/** 分页查询活动配置。 */
public PageResult<RewardActivityRespVO> page(RewardActivityQueryReqVO req) {
Page<RewardActivity> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
IPage<RewardActivity> result = activityMapper.selectPage(page, Wrappers.<RewardActivity>lambdaQuery()
.eq(RewardActivity::getDeleted, 0)
.eq(req.getStatus() != null, RewardActivity::getStatus, req.getStatus())
.and(StringUtils.hasText(req.getKeyword()), w -> w
.like(RewardActivity::getCode, req.getKeyword())
.or().like(RewardActivity::getTitleId, req.getKeyword())
.or().like(RewardActivity::getTitleEn, req.getKeyword())
.or().like(RewardActivity::getTitleZh, req.getKeyword()))
.orderByAsc(RewardActivity::getSort)
.orderByDesc(RewardActivity::getId));
return new PageResult<>(result.getTotal(), result.getRecords().stream()
.map(RewardActivityRespVO::of).toList());
}
/** 查询活动详情。 */
public RewardActivityRespVO detail(Long id) {
return RewardActivityRespVO.of(getExisting(id));
}
/** 新建活动,默认下线,避免半配置内容立即展示。 */
@Transactional(rollbackFor = Exception.class)
public Long create(RewardActivitySaveReqVO req) {
validate(req);
String code = req.getCode().trim();
requireCodeFree(code, null);
RewardActivity activity = new RewardActivity();
activity.setCode(code);
apply(activity, req);
activity.setStatus(0);
activityMapper.insert(activity);
return activity.getId();
}
/** 编辑活动,保留现有上下线状态。 */
@Transactional(rollbackFor = Exception.class)
public void update(Long id, RewardActivitySaveReqVO req) {
validate(req);
RewardActivity activity = getExisting(id);
String code = req.getCode().trim();
requireCodeFree(code, id);
activity.setCode(code);
apply(activity, req);
activityMapper.updateById(activity);
}
/** 上线或下线活动。 */
@Transactional(rollbackFor = Exception.class)
public void updateStatus(Long id, Integer status) {
if (status == null || (status != 0 && status != 1)) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "状态只能是 0 或 1");
}
RewardActivity activity = getExisting(id);
activity.setStatus(status);
activityMapper.updateById(activity);
}
private void validate(RewardActivitySaveReqVO req) {
if (req == null || !StringUtils.hasText(req.getCode()) || !StringUtils.hasText(req.getTitleId())) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "请填写活动编码和印尼语标题");
}
}
private void apply(RewardActivity activity, RewardActivitySaveReqVO req) {
activity.setCoverUrl(valueOrEmpty(req.getCoverUrl()));
activity.setActionType(valueOrDefault(req.getActionType(), "NONE"));
activity.setActionUrl(valueOrEmpty(req.getActionUrl()));
activity.setTitleId(req.getTitleId().trim());
activity.setTitleEn(valueOrEmpty(req.getTitleEn()));
activity.setTitleZh(valueOrEmpty(req.getTitleZh()));
activity.setSummaryId(valueOrEmpty(req.getSummaryId()));
activity.setSummaryEn(valueOrEmpty(req.getSummaryEn()));
activity.setSummaryZh(valueOrEmpty(req.getSummaryZh()));
activity.setCtaId(valueOrEmpty(req.getCtaId()));
activity.setCtaEn(valueOrEmpty(req.getCtaEn()));
activity.setCtaZh(valueOrEmpty(req.getCtaZh()));
activity.setSort(req.getSort() == null ? 0 : req.getSort());
activity.setStartAt(req.getStartAt());
activity.setEndAt(req.getEndAt());
}
private RewardActivity getExisting(Long id) {
RewardActivity activity = id == null ? null : activityMapper.selectById(id);
if (activity == null || Integer.valueOf(1).equals(activity.getDeleted())) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "奖励活动不存在");
}
return activity;
}
private void requireCodeFree(String code, Long excludeId) {
Long count = activityMapper.selectCount(Wrappers.<RewardActivity>lambdaQuery()
.eq(RewardActivity::getCode, code)
.eq(RewardActivity::getDeleted, 0)
.ne(excludeId != null, RewardActivity::getId, excludeId));
if (count != null && count > 0) {
throw new ServiceException(ErrorCode.DATA_EXISTS, "活动编码已存在:" + code);
}
}
private String valueOrEmpty(String value) {
return value == null ? "" : value.trim();
}
private String valueOrDefault(String value, String fallback) {
return StringUtils.hasText(value) ? value.trim() : fallback;
}
}
@@ -0,0 +1,16 @@
// Author: fruit.com.cn
package com.fruitshorts.commerce.reward.vo;
import com.fruitshorts.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 后台奖励活动分页条件。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class RewardActivityQueryReqVO extends PageParam {
private String keyword;
private Integer status;
}
@@ -0,0 +1,62 @@
// Author: fruit.com.cn
package com.fruitshorts.commerce.reward.vo;
import com.fruitshorts.commerce.reward.dal.entity.RewardActivity;
import lombok.Data;
import java.time.LocalDateTime;
/** 后台奖励活动响应。 */
@Data
public class RewardActivityRespVO {
private Long id;
private String code;
private String coverUrl;
private String actionType;
private String actionUrl;
private String titleId;
private String titleEn;
private String titleZh;
private String summaryId;
private String summaryEn;
private String summaryZh;
private String ctaId;
private String ctaEn;
private String ctaZh;
private Integer sort;
private Integer status;
private String statusText;
private LocalDateTime startAt;
private LocalDateTime endAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/** 将活动实体转换为后台展示模型。 */
public static RewardActivityRespVO of(RewardActivity activity) {
RewardActivityRespVO vo = new RewardActivityRespVO();
vo.id = activity.getId();
vo.code = activity.getCode();
vo.coverUrl = activity.getCoverUrl();
vo.actionType = activity.getActionType();
vo.actionUrl = activity.getActionUrl();
vo.titleId = activity.getTitleId();
vo.titleEn = activity.getTitleEn();
vo.titleZh = activity.getTitleZh();
vo.summaryId = activity.getSummaryId();
vo.summaryEn = activity.getSummaryEn();
vo.summaryZh = activity.getSummaryZh();
vo.ctaId = activity.getCtaId();
vo.ctaEn = activity.getCtaEn();
vo.ctaZh = activity.getCtaZh();
vo.sort = activity.getSort();
vo.status = activity.getStatus();
vo.statusText = activity.getStatus() != null && activity.getStatus() == 1 ? "已上线" : "已下线";
vo.startAt = activity.getStartAt();
vo.endAt = activity.getEndAt();
vo.createdAt = activity.getCreatedAt();
vo.updatedAt = activity.getUpdatedAt();
return vo;
}
}
@@ -0,0 +1,36 @@
// Author: fruit.com.cn
package com.fruitshorts.commerce.reward.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
/** 后台奖励活动新增和编辑参数。 */
@Data
public class RewardActivitySaveReqVO {
@NotBlank(message = "请填写活动编码")
@Size(max = 64, message = "活动编码过长")
private String code;
@Size(max = 512, message = "封面地址过长")
private String coverUrl;
private String actionType;
@Size(max = 512, message = "跳转地址过长")
private String actionUrl;
@NotBlank(message = "请填写印尼语标题")
private String titleId;
private String titleEn;
private String titleZh;
private String summaryId;
private String summaryEn;
private String summaryZh;
private String ctaId;
private String ctaEn;
private String ctaZh;
private Integer sort;
private LocalDateTime startAt;
private LocalDateTime endAt;
}
@@ -0,0 +1,102 @@
// Author: fruit.com.cn
package com.fruitshorts.commerce.reward.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fruitshorts.commerce.reward.dal.entity.RewardActivity;
import com.fruitshorts.commerce.reward.dal.mapper.RewardActivityMapper;
import com.fruitshorts.commerce.reward.vo.RewardActivitySaveReqVO;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/** 奖励活动后台配置服务测试。 */
class RewardActivityAdminServiceTest {
private RewardActivityMapper activityMapper;
private RewardActivityAdminService service;
@BeforeAll
static void initTableInfo() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""), RewardActivity.class);
}
@BeforeEach
void setUp() {
activityMapper = mock(RewardActivityMapper.class);
service = new RewardActivityAdminService(activityMapper);
}
@Test
void should_create_activity_offline_when_status_is_missing() {
given(activityMapper.selectCount(any())).willReturn(0L);
AtomicReference<RewardActivity> saved = captureInsert();
service.create(activity("spring_event"));
assertThat(saved.get().getCode()).isEqualTo("spring_event");
assertThat(saved.get().getStatus()).isZero();
}
@Test
void should_reject_when_activity_code_already_exists() {
given(activityMapper.selectCount(any())).willReturn(1L);
assertThatThrownBy(() -> service.create(activity("spring_event")))
.isInstanceOf(ServiceException.class)
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
.isEqualTo(ErrorCode.DATA_EXISTS.getCode()))
.hasMessageContaining("活动编码已存在");
verify(activityMapper, never()).insert(any(RewardActivity.class));
}
@Test
void should_update_status_when_status_is_valid() {
RewardActivity existing = new RewardActivity();
existing.setId(9L);
existing.setDeleted(0);
given(activityMapper.selectById(9L)).willReturn(existing);
service.updateStatus(9L, 1);
assertThat(existing.getStatus()).isOne();
verify(activityMapper).updateById(existing);
}
private AtomicReference<RewardActivity> captureInsert() {
AtomicReference<RewardActivity> saved = new AtomicReference<>();
given(activityMapper.insert(any(RewardActivity.class))).willAnswer(invocation -> {
RewardActivity activity = invocation.getArgument(0);
activity.setId(9L);
saved.set(activity);
return 1;
});
return saved;
}
private RewardActivitySaveReqVO activity(String code) {
RewardActivitySaveReqVO req = new RewardActivitySaveReqVO();
req.setCode(code);
req.setTitleId("Acara musim semi");
req.setTitleEn("Spring event");
req.setTitleZh("春季活动");
req.setSummaryId("Dapatkan koin tambahan");
req.setActionType("OPEN_HOME");
return req;
}
}