(〃'▽'〃)_v2026_07_发布:管理后台移动端完成全模块适配,增强工作流与 CRM 数据统计

This commit is contained in:
YunaiV
2026-07-31 19:57:36 +08:00
parent dcbeabb988
commit d9fc5fd7ff
135 changed files with 0 additions and 11709 deletions
@@ -1,295 +0,0 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.core.collection.ListUtil;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* {@link MultiDictConvert} 的单元测试
*
* @author NaCl
*/
public class MultiDictConvertTest extends BaseMockitoUnitTest {
private static final String DICT_TYPE = "test_dict";
@Mock
private DictDataCommonApi dictDataApi;
private MultiDictConvert convert;
@BeforeEach
public void setUp() {
convert = new MultiDictConvert();
DictFrameworkUtils.init(dictDataApi);
DictFrameworkUtils.clearCache();
}
@Test
public void testConvertToExcelData_string() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("1,2", mockContentProperty("stringValues"), null);
// 断言
assertEquals("男、女", result.getStringValue());
}
@Test
public void testConvertToExcelData_single() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("1", mockContentProperty("stringValues"), null);
// 断言
assertEquals("男", result.getStringValue());
}
@Test
public void testConvertToExcelData_order() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("2,1", mockContentProperty("stringValues"), null);
// 断言
assertEquals("女、男", result.getStringValue());
}
@Test
public void testConvertToExcelData_null() {
// 调用
WriteCellData<String> result = convert.convertToExcelData(null, null, null);
// 断言
assertEquals("", result.getStringValue());
}
@Test
public void testConvertToExcelData_collection() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData(ListUtil.of(1, 2),
mockContentProperty("listValues"), null);
// 断言
assertEquals("男、女", result.getStringValue());
}
@Test
public void testConvertToExcelData_array() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData(new int[]{1, 2},
mockContentProperty("stringValues"), null);
// 断言
assertEquals("男、女", result.getStringValue());
}
@Test
public void testConvertToExcelData_unknownValue() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("1,3", mockContentProperty("stringValues"), null);
// 断言
assertEquals("", result.getStringValue());
}
@Test
public void testConvertToJavaData_string() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
mockContentProperty("stringValues"), null);
// 断言
assertEquals("1,2", result);
}
@Test
public void testConvertToJavaData_single() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男"),
mockContentProperty("stringValues"), null);
// 断言
assertEquals("1", result);
}
@Test
public void testConvertToJavaData_blank() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("stringValues"), null);
// 断言
assertEquals("", result);
}
@Test
public void testConvertToJavaData_blankList() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("listValues"), null);
// 断言
assertTrue(result instanceof List);
assertTrue(((List<?>) result).isEmpty());
}
@Test
public void testConvertToJavaData_blankSet() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("setValues"), null);
// 断言
assertTrue(result instanceof LinkedHashSet);
assertTrue(((Set<?>) result).isEmpty());
}
@Test
public void testConvertToJavaData_blankArray() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("arrayValues"), null);
// 断言
assertArrayEquals(new Integer[0], (Integer[]) result);
}
@Test
@SuppressWarnings("unchecked")
public void testConvertToJavaData_list() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
mockContentProperty("listValues"), null);
// 断言
assertEquals(ListUtil.of(1, 2), (List<Integer>) result);
}
@Test
@SuppressWarnings("unchecked")
public void testConvertToJavaData_set() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
mockContentProperty("setValues"), null);
// 断言
assertEquals(new LinkedHashSet<>(Arrays.asList(1L, 2L)), (Set<Long>) result);
}
@Test
@SuppressWarnings("SimplifiableAssertion")
public void testConvertToJavaData_hashSet() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
mockContentProperty("hashSetValues"), null);
// 断言
assertTrue(result instanceof HashSet);
assertEquals(new HashSet<>(Arrays.asList(1L, 2L)), result);
}
@Test
public void testConvertToJavaData_array() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
mockContentProperty("arrayValues"), null);
// 断言
assertArrayEquals(new Integer[]{1, 2}, (Integer[]) result);
}
@Test
public void testConvertToJavaData_unknownLabel() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男、未知"),
mockContentProperty("stringValues"), null);
// 断言
assertNull(result);
}
private void mockDictDatas() {
List<DictDataRespDTO> dictDatas = ListUtil.of(
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("1").setLabel("男")),
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("2").setLabel("女"))
);
when(dictDataApi.getDictDataList(eq(DICT_TYPE))).thenReturn(dictDatas);
}
private static ReadCellData<?> mockReadCellData(String value) {
ReadCellData<?> readCellData = mock(ReadCellData.class);
when(readCellData.getStringValue()).thenReturn(value);
return readCellData;
}
private static ExcelContentProperty mockContentProperty(String fieldName) throws NoSuchFieldException {
Field field = TestExcelVO.class.getDeclaredField(fieldName);
ExcelContentProperty contentProperty = mock(ExcelContentProperty.class);
when(contentProperty.getField()).thenReturn(field);
return contentProperty;
}
static class TestExcelVO {
@DictFormat(DICT_TYPE)
private String stringValues;
@DictFormat(DICT_TYPE)
private List<Integer> listValues;
@DictFormat(DICT_TYPE)
private Set<Long> setValues;
@DictFormat(DICT_TYPE)
private HashSet<Long> hashSetValues;
@DictFormat(DICT_TYPE)
private Integer[] arrayValues;
}
}
@@ -1,81 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.comment;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentRespVO;
import cn.iocoder.yudao.module.bpm.service.comment.BpmCommentService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.flowable.engine.task.Comment;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - 流程评论")
@RestController
@RequestMapping("/bpm/comment")
@Validated
public class BpmCommentController {
@Resource
private BpmCommentService commentService;
@Resource
private BpmTaskService taskService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/list-by-process-instance-id")
@Operation(summary = "获得指定流程实例的评论列表")
@Parameter(name = "processInstanceId", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
public CommonResult<List<BpmCommentRespVO>> getCommentListByProcessInstanceId(
@RequestParam("processInstanceId") String processInstanceId) {
// 获得评论列表
List<Comment> commentList = commentService.getCommentListByProcessInstanceId(processInstanceId);
if (CollUtil.isEmpty(commentList)) {
return success(Collections.emptyList());
}
// 拼接 VO
Map<String, HistoricTaskInstance> taskMap = taskService.getHistoricTaskMap(
convertSet(commentList, Comment::getTaskId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(commentList, comment -> NumberUtils.parseLong(comment.getUserId())));
return success(convertList(commentList, comment -> BeanUtils.toBean(comment, BpmCommentRespVO.class, commentVO -> {
commentVO.setMessage(comment.getFullMessage())
.setCreateTime(DateUtils.of(comment.getTime()))
.setTask(BeanUtils.toBean(taskMap.get(comment.getTaskId()), BpmCommentRespVO.Task.class))
.setUser(BeanUtils.toBean(userMap.get(NumberUtils.parseLong(comment.getUserId())), UserSimpleBaseVO.class));
})));
}
@PostMapping("/create")
@Operation(summary = "创建流程评论")
@PreAuthorize("@ss.hasPermission('bpm:task:update')")
public CommonResult<Boolean> createComment(@Valid @RequestBody BpmCommentCreateReqVO reqVO) {
commentService.createComment(reqVO);
return success(true);
}
}
@@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.comment.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 流程评论创建 Request VO")
@Data
public class BpmCommentCreateReqVO {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotEmpty(message = "任务编号不能为空")
private String taskId;
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "请关注报销发票附件")
@NotBlank(message = "评论内容不能为空")
@Size(max = 500, message = "评论内容不能超过 500 个字符")
private String message;
}
@@ -1,52 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.comment.vo;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 流程评论 Response VO")
@Data
public class BpmCommentRespVO {
@Schema(description = "评论编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@Schema(description = "任务编号", example = "2048")
private String taskId;
@Schema(description = "任务")
private Task task;
@Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
private String processInstanceId;
@Schema(description = "评论类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private String type;
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "请关注报销发票附件")
private String message;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "创建人")
private UserSimpleBaseVO user;
@Schema(description = "任务")
@Data
public static class Task {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private String id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "经理审批")
private String name;
@Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one")
private String taskDefinitionKey;
}
}
@@ -1,42 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.comment;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import jakarta.validation.Valid;
import org.flowable.engine.task.Comment;
import java.util.List;
/**
* 流程评论 Service 接口
*
* @author 芋道源码
*/
public interface BpmCommentService {
/**
* 获得指定流程实例的评论列表
*
* @param processInstanceId 流程实例的编号
* @return 评论列表
*/
List<Comment> getCommentListByProcessInstanceId(String processInstanceId);
/**
* 创建流程评论
*
* @param reqVO 评论请求
*/
void createComment(@Valid BpmCommentCreateReqVO reqVO);
/**
* 创建流程评论
*
* @param taskId 任务编号
* @param processInstanceId 流程实例编号
* @param type 评论类型
* @param params 评论模板参数
*/
void createComment(String taskId, String processInstanceId, BpmCommentTypeEnum type, Object... params);
}
@@ -1,48 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.comment;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.flowable.engine.TaskService;
import org.flowable.engine.task.Comment;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 流程评论 Service 实现类
*
* @author 芋道源码
*/
@Service
public class BpmCommentServiceImpl implements BpmCommentService {
@Resource
private TaskService taskService;
@Resource
@Lazy // 延迟加载,避免循环依赖
private BpmTaskService bpmTaskService;
@Override
public List<Comment> getCommentListByProcessInstanceId(String processInstanceId) {
return taskService.getProcessInstanceComments(processInstanceId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createComment(@Valid BpmCommentCreateReqVO reqVO) {
Task task = bpmTaskService.validateTaskExists(reqVO.getTaskId());
createComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.COMMENT, reqVO.getMessage());
}
@Override
public void createComment(String taskId, String processInstanceId, BpmCommentTypeEnum type, Object... params) {
taskService.addComment(taskId, processInstanceId, type.getType(), type.formatComment(params));
}
}
@@ -1,175 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.definition;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link BpmModelServiceImpl} 的单元测试
*
* @author 芋道源码
*/
public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
private static final Long TENANT_ID = 1L;
private static final String MODEL_ID = "model-id";
@InjectMocks
private BpmModelServiceImpl modelService;
@Mock
private RepositoryService repositoryService;
@Mock
private ModelQuery modelQuery;
@BeforeEach
public void setUp() {
TenantContextHolder.setTenantId(TENANT_ID);
}
@AfterEach
public void tearDown() {
TenantContextHolder.clear();
}
@Test
public void testExportModel_bpmn() {
// 准备参数
Model model = mockModel(BpmModelTypeEnum.BPMN.getType());
String bpmnXml = "<definitions />";
// mock 方法(repositoryService)
mockGetModel(model);
when(repositoryService.getModelEditorSource(eq(MODEL_ID))).thenReturn(StrUtil.utf8Bytes(bpmnXml));
// 调用
BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
// 断言
assertEquals(model.getKey(), result.getKey());
assertEquals(model.getName(), result.getName());
assertEquals(model.getCategory(), result.getCategory());
assertEquals(BpmModelTypeEnum.BPMN.getType(), result.getType());
assertEquals(bpmnXml, result.getBpmnXml());
assertNull(result.getSimpleModel());
}
@Test
public void testExportModel_simple() {
// 准备参数
Model model = mockModel(BpmModelTypeEnum.SIMPLE.getType());
BpmSimpleModelNodeVO simpleModel = new BpmSimpleModelNodeVO();
simpleModel.setId("start");
simpleModel.setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType());
// mock 方法(repositoryService)
mockGetModel(model);
when(repositoryService.getModelEditorSourceExtra(eq(MODEL_ID)))
.thenReturn(JsonUtils.toJsonByte(simpleModel));
// 调用
BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
// 断言
assertEquals(BpmModelTypeEnum.SIMPLE.getType(), result.getType());
assertNotNull(result.getSimpleModel());
assertEquals(simpleModel.getId(), result.getSimpleModel().getId());
assertEquals(simpleModel.getType(), result.getSimpleModel().getType());
assertNull(result.getBpmnXml());
}
@Test
public void testExportModel_notExists() {
// mock 方法(repositoryService)
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
when(modelQuery.singleResult()).thenReturn(null);
// 调用,并断言异常
assertServiceException(() -> modelService.exportModel(MODEL_ID), MODEL_NOT_EXISTS);
}
@Test
public void testImportModel_bpmn() {
// 准备参数
BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
reqVO.setKey("test_process");
reqVO.setName("测试流程");
reqVO.setCategory("OA");
reqVO.setType(BpmModelTypeEnum.BPMN.getType());
reqVO.setBpmnXml("<definitions />");
reqVO.setStartUserIds(Arrays.asList(10L, 20L));
reqVO.setStartDeptIds(Collections.singletonList(30L));
reqVO.setManagerUserIds(Collections.singletonList(40L));
Model model = mock(Model.class);
when(model.getId()).thenReturn(MODEL_ID);
// mock 方法(repositoryService)
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
when(modelQuery.modelKey(eq(reqVO.getKey()))).thenReturn(modelQuery);
when(modelQuery.singleResult()).thenReturn(null);
when(repositoryService.newModel()).thenReturn(model);
// 调用
String result = modelService.importModel(reqVO);
// 断言
assertEquals(MODEL_ID, result);
verify(model).setTenantId(TENANT_ID.toString());
verify(repositoryService).saveModel(same(model));
verify(repositoryService).addModelEditorSource(eq(MODEL_ID),
aryEq(StrUtil.utf8Bytes(reqVO.getBpmnXml())));
ArgumentCaptor<String> metaInfoCaptor = ArgumentCaptor.forClass(String.class);
verify(model).setMetaInfo(metaInfoCaptor.capture());
BpmModelMetaInfoVO metaInfo = JsonUtils.parseObject(metaInfoCaptor.getValue(), BpmModelMetaInfoVO.class);
assertNotNull(metaInfo);
assertEquals(Arrays.asList(10L, 20L), metaInfo.getStartUserIds());
assertEquals(Collections.singletonList(30L), metaInfo.getStartDeptIds());
assertEquals(Collections.singletonList(40L), metaInfo.getManagerUserIds());
}
private Model mockModel(Integer type) {
Model model = mock(Model.class);
when(model.getKey()).thenReturn("test_process");
when(model.getName()).thenReturn("测试流程");
when(model.getCategory()).thenReturn("OA");
when(model.getCreateTime()).thenReturn(new Date(1_000L));
BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
metaInfo.setType(type);
when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
return model;
}
private void mockGetModel(Model model) {
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
when(modelQuery.singleResult()).thenReturn(model);
}
}
@@ -1,149 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import cn.iocoder.yudao.module.crm.service.performance.CrmPerformanceConfigService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@Tag(name = "管理后台 - CRM 业绩目标设置")
@RestController
@RequestMapping("/crm/performance-config")
@Validated
public class CrmPerformanceConfigController {
@Resource
private CrmPerformanceConfigService performanceConfigService;
@Resource
private DeptApi deptApi;
@Resource
private AdminUserApi adminUserApi;
@PostMapping("/create")
@Operation(summary = "创建业绩目标")
@PreAuthorize("@ss.hasPermission('crm:performance-config:create')")
public CommonResult<Long> createPerformanceConfig(@Valid @RequestBody CrmPerformanceConfigSaveReqVO createReqVO) {
return success(performanceConfigService.createPerformanceConfig(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新业绩目标")
@PreAuthorize("@ss.hasPermission('crm:performance-config:update')")
public CommonResult<Boolean> updatePerformanceConfig(@Valid @RequestBody CrmPerformanceConfigSaveReqVO updateReqVO) {
performanceConfigService.updatePerformanceConfig(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除业绩目标")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('crm:performance-config:delete')")
public CommonResult<Boolean> deletePerformanceConfig(@RequestParam("id") Long id) {
performanceConfigService.deletePerformanceConfig(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得业绩目标")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('crm:performance-config:query')")
public CommonResult<CrmPerformanceConfigRespVO> getPerformanceConfig(@RequestParam("id") Long id) {
// 1. 获取业绩目标
CrmPerformanceConfigDO performanceConfig = performanceConfigService.getPerformanceConfig(id);
// 2. 拼接数据
return success(buildPerformanceConfigDetail(performanceConfig));
}
private CrmPerformanceConfigRespVO buildPerformanceConfigDetail(CrmPerformanceConfigDO performanceConfig) {
if (performanceConfig == null) {
return null;
}
// 1. 转换成 VO
CrmPerformanceConfigRespVO respVO = BeanUtils.toBean(performanceConfig, CrmPerformanceConfigRespVO.class);
// 2. 拼接目标对象名称
return buildPerformanceConfigObjectName(respVO);
}
@GetMapping("/page")
@Operation(summary = "获得业绩目标分页")
@PreAuthorize("@ss.hasPermission('crm:performance-config:query')")
public CommonResult<PageResult<CrmPerformanceConfigRespVO>> getPerformanceConfigPage(@Valid CrmPerformanceConfigPageReqVO pageReqVO) {
// 1. 查询业绩目标分页
PageResult<CrmPerformanceConfigDO> pageResult = performanceConfigService.getPerformanceConfigPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2. 拼接数据
return success(BeanUtils.toBean(pageResult, CrmPerformanceConfigRespVO.class)
.setList(buildPerformanceConfigDetailList(pageResult.getList())));
}
private List<CrmPerformanceConfigRespVO> buildPerformanceConfigDetailList(List<CrmPerformanceConfigDO> list) {
if (CollUtil.isEmpty(list)) {
return emptyList();
}
// 1. 转换成 VO
List<CrmPerformanceConfigRespVO> voList = BeanUtils.toBean(list, CrmPerformanceConfigRespVO.class);
// 2. 拼接目标对象名称
return buildPerformanceConfigObjectNameList(voList);
}
// ========== VO 拼装 ==========
private CrmPerformanceConfigRespVO buildPerformanceConfigObjectName(CrmPerformanceConfigRespVO config) {
if (config == null) {
return null;
}
return buildPerformanceConfigObjectNameList(singletonList(config)).get(0);
}
private List<CrmPerformanceConfigRespVO> buildPerformanceConfigObjectNameList(List<CrmPerformanceConfigRespVO> list) {
if (CollUtil.isEmpty(list)) {
return emptyList();
}
// 1.1 获取部门列表
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(list, CrmPerformanceConfigRespVO::getObjectId,
item -> Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType())));
// 1.2 获取员工列表
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(list, CrmPerformanceConfigRespVO::getObjectId,
item -> Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.USER.getObjectType())));
// 2. 设置目标对象名称
list.forEach(item -> {
if (Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType())) {
findAndThen(deptMap, item.getObjectId(), dept -> item.setObjectName(dept.getName()));
} else if (Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.USER.getObjectType())) {
findAndThen(userMap, item.getObjectId(), user -> item.setObjectName(user.getNickname()));
}
});
return list;
}
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - CRM 业绩目标设置分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CrmPerformanceConfigPageReqVO extends PageParam {
@Schema(description = "目标对象编号", example = "1")
private Long objectId;
@Schema(description = "目标对象类型", example = "2")
@InEnum(value = CrmPerformanceConfigObjectTypeEnum.class, message = "目标对象类型,必须是 {value}")
private Integer objectType;
@Schema(description = "年份", example = "2026")
private Integer year;
@Schema(description = "目标类型", example = "5")
@InEnum(value = CrmPerformanceConfigBizTypeEnum.class, message = "目标类型,必须是 {value}")
private Integer bizType;
}
@@ -1,74 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - CRM 业绩目标设置 Response VO")
@Data
public class CrmPerformanceConfigRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "目标对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long objectId;
@Schema(description = "目标对象名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "销售部")
private String objectName;
@Schema(description = "目标对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer objectType;
@Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
private Integer year;
@Schema(description = "一月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal januaryTargetPrice;
@Schema(description = "二月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal februaryTargetPrice;
@Schema(description = "三月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal marchTargetPrice;
@Schema(description = "四月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal aprilTargetPrice;
@Schema(description = "五月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal mayTargetPrice;
@Schema(description = "六月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal juneTargetPrice;
@Schema(description = "七月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal julyTargetPrice;
@Schema(description = "八月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal augustTargetPrice;
@Schema(description = "九月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal septemberTargetPrice;
@Schema(description = "十月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal octoberTargetPrice;
@Schema(description = "十一月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal novemberTargetPrice;
@Schema(description = "十二月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal decemberTargetPrice;
@Schema(description = "目标类型",
requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
private Integer bizType;
@Schema(description = "年度目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal yearTargetPrice;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -1,74 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 业绩目标设置新增/修改 Request VO")
@Data
public class CrmPerformanceConfigSaveReqVO {
@Schema(description = "编号", example = "1")
private Long id;
@Schema(description = "目标对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "目标对象编号不能为空")
private Long objectId;
@Schema(description = "目标对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "目标对象类型不能为空")
@InEnum(value = CrmPerformanceConfigObjectTypeEnum.class, message = "目标对象类型,必须是 {value}")
private Integer objectType;
@Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
@NotNull(message = "年份不能为空")
private Integer year;
@Schema(description = "一月目标金额")
private BigDecimal januaryTargetPrice;
@Schema(description = "二月目标金额")
private BigDecimal februaryTargetPrice;
@Schema(description = "三月目标金额")
private BigDecimal marchTargetPrice;
@Schema(description = "四月目标金额")
private BigDecimal aprilTargetPrice;
@Schema(description = "五月目标金额")
private BigDecimal mayTargetPrice;
@Schema(description = "六月目标金额")
private BigDecimal juneTargetPrice;
@Schema(description = "七月目标金额")
private BigDecimal julyTargetPrice;
@Schema(description = "八月目标金额")
private BigDecimal augustTargetPrice;
@Schema(description = "九月目标金额")
private BigDecimal septemberTargetPrice;
@Schema(description = "十月目标金额")
private BigDecimal octoberTargetPrice;
@Schema(description = "十一月目标金额")
private BigDecimal novemberTargetPrice;
@Schema(description = "十二月目标金额")
private BigDecimal decemberTargetPrice;
@Schema(description = "目标类型",
requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
@NotNull(message = "目标类型不能为空")
@InEnum(value = CrmPerformanceConfigBizTypeEnum.class, message = "目标类型,必须是 {value}")
private Integer bizType;
}
@@ -1,37 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetRespVO;
import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsPerformanceTargetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - CRM 业绩目标统计")
@RestController
@RequestMapping("/crm/statistics-performance-target")
@Validated
public class CrmStatisticsPerformanceTargetController {
@Resource
private CrmStatisticsPerformanceTargetService performanceTargetService;
@GetMapping("/get-performance-target-summary")
@Operation(summary = "获取业绩目标完成情况")
@PreAuthorize("@ss.hasPermission('crm:statistics-performance-target:query')")
public CommonResult<List<CrmStatisticsPerformanceTargetRespVO>> getPerformanceTargetSummary(@Valid CrmStatisticsPerformanceTargetReqVO reqVO) {
return success(performanceTargetService.getPerformanceTargetSummary(reqVO));
}
}
@@ -1,45 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - CRM 产品统计")
@RestController
@RequestMapping("/crm/statistics-product")
@Validated
public class CrmStatisticsProductController {
@Resource
private CrmStatisticsProductService productService;
@GetMapping("/get-product-sales-list")
@Operation(summary = "获取产品销售情况统计")
@PreAuthorize("@ss.hasPermission('crm:statistics-product:query')")
public CommonResult<List<CrmStatisticsProductSalesRespVO>> getProductSalesList(@Valid CrmStatisticsProductReqVO reqVO) {
return success(productService.getProductSalesList(reqVO));
}
@GetMapping("/get-product-category-summary")
@Operation(summary = "获取产品分类销售分析")
@PreAuthorize("@ss.hasPermission('crm:statistics-product:query')")
public CommonResult<List<CrmStatisticsProductCategoryRespVO>> getProductCategorySummary(@Valid CrmStatisticsProductReqVO reqVO) {
return success(productService.getProductCategorySummary(reqVO));
}
}
@@ -1,34 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 商机阶段统计 Response VO")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CrmStatisticsBusinessSummaryByStatusRespVO {
@Schema(description = "商机阶段编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long statusId;
@Schema(description = "商机阶段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "初步接洽")
private String statusName;
@Schema(description = "赢单率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
private Integer statusPercent;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer sort;
@Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long businessCount;
@Schema(description = "商机总金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private BigDecimal totalPrice;
}
@@ -1,27 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 员工业绩合同汇总 Response VO")
@Data
public class CrmStatisticsPerformanceSummaryRespVO {
@Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
private String time;
@Schema(description = "合同数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer contractCount;
@Schema(description = "合同金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00")
private BigDecimal contractPrice;
@Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00")
private BigDecimal receivablePrice;
@Schema(description = "未回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000.00")
private BigDecimal unreceivedPrice;
}
@@ -1,20 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 业绩目标完成实际值 Response VO")
@Data
@Accessors(chain = true)
public class CrmStatisticsPerformanceTargetActualRespVO {
@Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer month;
@Schema(description = "完成金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00")
private BigDecimal currentPrice;
}
@@ -1,35 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - CRM 业绩目标完成情况 Request VO")
@Data
public class CrmStatisticsPerformanceTargetReqVO {
@Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门 id 不能为空")
private Long deptId;
@Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
private Long userId;
@Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
private List<Long> userIds;
@Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024")
@NotNull(message = "年份不能为空")
private Integer year;
@Schema(description = "目标类型",
requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
@NotNull(message = "目标类型不能为空")
@InEnum(value = CrmPerformanceConfigBizTypeEnum.class, message = "目标类型,必须是 {value}")
private Integer bizType;
}
@@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 业绩目标完成情况 Response VO")
@Data
@Accessors(chain = true)
public class CrmStatisticsPerformanceTargetRespVO {
@Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer month;
@Schema(description = "目标金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00")
private BigDecimal targetPrice;
@Schema(description = "完成金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00")
private BigDecimal currentPrice;
@Schema(description = "完成率", requiredMode = Schema.RequiredMode.REQUIRED, example = "80.00")
private BigDecimal completionRate;
}
@@ -1,27 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 产品分类销售分析 Response VO")
@Data
public class CrmStatisticsProductCategoryRespVO {
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long categoryId;
@Schema(description = "产品分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "软件")
private String categoryName;
@Schema(description = "合同数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Integer contractCount;
@Schema(description = "销售数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
private BigDecimal productCount;
@Schema(description = "销售金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000.00")
private BigDecimal productTotalPrice;
}
@@ -1,44 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - CRM 产品分析 Request VO")
@Data
public class CrmStatisticsProductReqVO {
@Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门 id 不能为空")
private Long deptId;
@Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
private Long userId;
@Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
private List<Long> userIds;
@Schema(description = "时间范围", requiredMode = Schema.RequiredMode.REQUIRED)
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@NotEmpty(message = "请选择时间范围")
@Size(min = 2, max = 2, message = "请选择时间范围")
private LocalDateTime[] times;
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "10")
private Long categoryId;
@Schema(description = "产品分类编号集合", hidden = true, example = "10")
private List<Long> categoryIds;
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "20")
private Long productId;
}
@@ -1,54 +0,0 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 产品销售情况 Response VO")
@Data
public class CrmStatisticsProductSalesRespVO {
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long categoryId;
@Schema(description = "产品分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "软件")
private String categoryName;
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
private Long productId;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CRM 专业版")
private String productName;
@Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30")
private Long contractId;
@Schema(description = "合同编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "HT20240101001")
private String contractNo;
@Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CRM 采购合同")
private String contractName;
@Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long ownerUserId;
@Schema(description = "负责人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String ownerUserName;
@Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "40")
private Long customerId;
@Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试客户")
private String customerName;
@Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal productPrice;
@Schema(description = "销售数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private BigDecimal productCount;
@Schema(description = "销售金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "200.00")
private BigDecimal productTotalPrice;
}
@@ -1,107 +0,0 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.performance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
* CRM 业绩目标 DO
*
* @author 芋道源码
*/
@TableName("crm_performance_config")
@KeySequence("crm_performance_config_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmPerformanceConfigDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 目标类型
*
* 枚举 {@link CrmPerformanceConfigBizTypeEnum}
*/
private Integer bizType;
/**
* 目标对象编号
*/
private Long objectId;
/**
* 目标对象类型
*
* 枚举 {@link CrmPerformanceConfigObjectTypeEnum}
*/
private Integer objectType;
/**
* 年份
*/
private Integer year;
/**
* 年度目标金额
*/
private BigDecimal yearTargetPrice;
/**
* 一月目标金额
*/
private BigDecimal januaryTargetPrice;
/**
* 二月目标金额
*/
private BigDecimal februaryTargetPrice;
/**
* 三月目标金额
*/
private BigDecimal marchTargetPrice;
/**
* 四月目标金额
*/
private BigDecimal aprilTargetPrice;
/**
* 五月目标金额
*/
private BigDecimal mayTargetPrice;
/**
* 六月目标金额
*/
private BigDecimal juneTargetPrice;
/**
* 七月目标金额
*/
private BigDecimal julyTargetPrice;
/**
* 八月目标金额
*/
private BigDecimal augustTargetPrice;
/**
* 九月目标金额
*/
private BigDecimal septemberTargetPrice;
/**
* 十月目标金额
*/
private BigDecimal octoberTargetPrice;
/**
* 十一月目标金额
*/
private BigDecimal novemberTargetPrice;
/**
* 十二月目标金额
*/
private BigDecimal decemberTargetPrice;
}
@@ -1,49 +0,0 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.permission;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* CRM 负责人变更记录 DO
*
* @author 芋道源码
*/
@TableName("crm_owner_record")
@KeySequence("crm_owner_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmOwnerRecordDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* CRM 业务类型
*
* 枚举 {@link CrmBizTypeEnum}
*/
private Integer bizType;
/**
* CRM 业务编号
*/
private Long bizId;
/**
* 变更前负责人
*/
private Long preOwnerUserId;
/**
* 变更后负责人
*/
private Long postOwnerUserId;
}
@@ -1,53 +0,0 @@
package cn.iocoder.yudao.module.crm.dal.mysql.performance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
* CRM 业绩目标配置 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmPerformanceConfigMapper extends BaseMapperX<CrmPerformanceConfigDO> {
default PageResult<CrmPerformanceConfigDO> selectPage(CrmPerformanceConfigPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<CrmPerformanceConfigDO>()
.eqIfPresent(CrmPerformanceConfigDO::getObjectId, reqVO.getObjectId())
.eqIfPresent(CrmPerformanceConfigDO::getObjectType, reqVO.getObjectType())
.eqIfPresent(CrmPerformanceConfigDO::getYear, reqVO.getYear())
.eqIfPresent(CrmPerformanceConfigDO::getBizType, reqVO.getBizType())
.orderByDesc(CrmPerformanceConfigDO::getYear)
.orderByAsc(CrmPerformanceConfigDO::getObjectType)
.orderByAsc(CrmPerformanceConfigDO::getObjectId)
.orderByAsc(CrmPerformanceConfigDO::getBizType));
}
default CrmPerformanceConfigDO selectByObjectIdAndObjectTypeAndYearAndBizType(Long objectId, Integer objectType,
Integer year, Integer bizType) {
return selectOne(new LambdaQueryWrapperX<CrmPerformanceConfigDO>()
.eq(CrmPerformanceConfigDO::getObjectId, objectId)
.eq(CrmPerformanceConfigDO::getObjectType, objectType)
.eq(CrmPerformanceConfigDO::getYear, year)
.eq(CrmPerformanceConfigDO::getBizType, bizType));
}
default List<CrmPerformanceConfigDO> selectListByObjectIdsAndObjectTypeAndYearAndBizType(Collection<Long> objectIds,
Integer objectType,
Integer year,
Integer bizType) {
return selectList(new LambdaQueryWrapperX<CrmPerformanceConfigDO>()
.in(CrmPerformanceConfigDO::getObjectId, objectIds)
.eq(CrmPerformanceConfigDO::getObjectType, objectType)
.eq(CrmPerformanceConfigDO::getYear, year)
.eq(CrmPerformanceConfigDO::getBizType, bizType));
}
}
@@ -1,52 +0,0 @@
package cn.iocoder.yudao.module.crm.dal.mysql.permission;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmOwnerRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* CRM 负责人变更记录 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmOwnerRecordMapper extends BaseMapperX<CrmOwnerRecordDO> {
/**
* 进入公海客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> selectPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> selectPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 进入公海客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> selectPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> selectPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO);
}
@@ -1,33 +0,0 @@
package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetActualRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* CRM 业绩目标完成情况 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmStatisticsPerformanceTargetMapper {
/**
* 按月统计合同金额
*
* @param reqVO 请求参数
* @return 按月统计合同金额
*/
List<CrmStatisticsPerformanceTargetActualRespVO> selectContractActualByMonth(CrmStatisticsPerformanceTargetReqVO reqVO);
/**
* 按月统计回款金额
*
* @param reqVO 请求参数
* @return 按月统计回款金额
*/
List<CrmStatisticsPerformanceTargetActualRespVO> selectReceivableActualByMonth(CrmStatisticsPerformanceTargetReqVO reqVO);
}
@@ -1,34 +0,0 @@
package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* CRM 产品分析 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmStatisticsProductMapper {
/**
* 获取产品销售情况统计
*
* @param reqVO 请求参数
* @return 产品销售情况
*/
List<CrmStatisticsProductSalesRespVO> selectProductSalesList(CrmStatisticsProductReqVO reqVO);
/**
* 获取产品分类销售分析
*
* @param reqVO 请求参数
* @return 产品分类销售分析
*/
List<CrmStatisticsProductCategoryRespVO> selectProductCategorySummary(CrmStatisticsProductReqVO reqVO);
}
@@ -1,39 +0,0 @@
package cn.iocoder.yudao.module.crm.enums.performance;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* CRM 业绩目标业务类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum CrmPerformanceConfigBizTypeEnum implements ArrayValuable<Integer> {
CRM_CONTRACT(CrmBizTypeEnum.CRM_CONTRACT.getType(), "销售目标"),
CRM_RECEIVABLE(CrmBizTypeEnum.CRM_RECEIVABLE.getType(), "回款目标");
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(CrmPerformanceConfigBizTypeEnum::getBizType).toArray(Integer[]::new);
/**
* 业务类型
*/
private final Integer bizType;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}
@@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.crm.enums.performance;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* CRM 业绩目标对象类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum CrmPerformanceConfigObjectTypeEnum implements ArrayValuable<Integer> {
DEPT(2, "部门"),
USER(3, "员工");
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(CrmPerformanceConfigObjectTypeEnum::getObjectType).toArray(Integer[]::new);
/**
* 对象类型
*/
private final Integer objectType;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}
@@ -1,80 +0,0 @@
package cn.iocoder.yudao.module.crm.service.performance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* CRM 业绩目标设置 Service 接口
*
* @author 芋道源码
*/
public interface CrmPerformanceConfigService {
/**
* 创建业绩目标
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createPerformanceConfig(@Valid CrmPerformanceConfigSaveReqVO createReqVO);
/**
* 更新业绩目标
*
* @param updateReqVO 更新信息
*/
void updatePerformanceConfig(@Valid CrmPerformanceConfigSaveReqVO updateReqVO);
/**
* 删除业绩目标
*
* @param id 编号
*/
void deletePerformanceConfig(Long id);
/**
* 获得业绩目标
*
* @param id 编号
* @return 业绩目标
*/
CrmPerformanceConfigDO getPerformanceConfig(Long id);
/**
* 获得业绩目标分页
*
* @param pageReqVO 分页查询
* @return 业绩目标分页
*/
PageResult<CrmPerformanceConfigDO> getPerformanceConfigPage(CrmPerformanceConfigPageReqVO pageReqVO);
/**
* 根据对象、年份、目标类型获得业绩目标配置
*
* @param objectId 对象编号
* @param objectType 对象类型
* @param year 年份
* @param bizType 业务类型
* @return 业绩目标配置
*/
CrmPerformanceConfigDO getPerformanceConfig(Long objectId, Integer objectType, Integer year, Integer bizType);
/**
* 根据对象数组、年份、目标类型获得业绩目标配置列表
*
* @param objectIds 对象编号数组
* @param objectType 对象类型
* @param year 年份
* @param bizType 业务类型
* @return 业绩目标配置列表
*/
List<CrmPerformanceConfigDO> getPerformanceConfigList(Collection<Long> objectIds, Integer objectType, Integer year,
Integer bizType);
}
@@ -1,170 +0,0 @@
package cn.iocoder.yudao.module.crm.service.performance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import cn.iocoder.yudao.module.crm.dal.mysql.performance.CrmPerformanceConfigMapper;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static cn.hutool.core.collection.CollUtil.isEmpty;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static java.util.Collections.singleton;
/**
* CRM 业绩目标设置 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmPerformanceConfigServiceImpl implements CrmPerformanceConfigService {
@Resource
private CrmPerformanceConfigMapper performanceConfigMapper;
@Resource
private DeptApi deptApi;
@Resource
private AdminUserApi adminUserApi;
@Override
public Long createPerformanceConfig(CrmPerformanceConfigSaveReqVO createReqVO) {
// 1.1 校验目标对象
validatePerformanceConfigObject(createReqVO.getObjectType(), createReqVO.getObjectId());
// 1.2 校验唯一
validatePerformanceConfigNotExists(null, createReqVO);
// 2. 插入
CrmPerformanceConfigDO performanceConfig = BeanUtils.toBean(createReqVO, CrmPerformanceConfigDO.class);
fillTargetPrice(performanceConfig);
performanceConfigMapper.insert(performanceConfig);
return performanceConfig.getId();
}
@Override
public void updatePerformanceConfig(CrmPerformanceConfigSaveReqVO updateReqVO) {
// 1.1 校验存在
validatePerformanceConfigExists(updateReqVO.getId());
// 1.2 校验目标对象
validatePerformanceConfigObject(updateReqVO.getObjectType(), updateReqVO.getObjectId());
// 1.3 校验唯一
validatePerformanceConfigNotExists(updateReqVO.getId(), updateReqVO);
// 2. 更新
CrmPerformanceConfigDO updateObj = BeanUtils.toBean(updateReqVO, CrmPerformanceConfigDO.class);
fillTargetPrice(updateObj);
performanceConfigMapper.updateById(updateObj);
}
@Override
public void deletePerformanceConfig(Long id) {
// 1. 校验存在
validatePerformanceConfigExists(id);
// 2. 删除
performanceConfigMapper.deleteById(id);
}
@Override
public CrmPerformanceConfigDO getPerformanceConfig(Long id) {
return validatePerformanceConfigExists(id);
}
@Override
public PageResult<CrmPerformanceConfigDO> getPerformanceConfigPage(CrmPerformanceConfigPageReqVO pageReqVO) {
return performanceConfigMapper.selectPage(pageReqVO);
}
private CrmPerformanceConfigDO validatePerformanceConfigExists(Long id) {
CrmPerformanceConfigDO performanceConfig = performanceConfigMapper.selectById(id);
if (performanceConfig == null) {
throw exception(PERFORMANCE_CONFIG_NOT_EXISTS);
}
return performanceConfig;
}
private void validatePerformanceConfigNotExists(Long id, CrmPerformanceConfigSaveReqVO reqVO) {
CrmPerformanceConfigDO performanceConfig = performanceConfigMapper.selectByObjectIdAndObjectTypeAndYearAndBizType(
reqVO.getObjectId(), reqVO.getObjectType(), reqVO.getYear(), reqVO.getBizType());
if (performanceConfig == null || Objects.equals(performanceConfig.getId(), id)) {
return;
}
throw exception(PERFORMANCE_CONFIG_EXISTS);
}
private void validatePerformanceConfigObject(Integer objectType, Long objectId) {
// 情况一:部门
if (Objects.equals(objectType, CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType())) {
deptApi.validateDeptList(singleton(objectId));
return;
}
// 情况二:员工
if (Objects.equals(objectType, CrmPerformanceConfigObjectTypeEnum.USER.getObjectType())) {
adminUserApi.validateUserList(singleton(objectId));
return;
}
throw exception(PERFORMANCE_CONFIG_TYPE_INVALID);
}
@Override
public CrmPerformanceConfigDO getPerformanceConfig(Long objectId, Integer objectType, Integer year, Integer bizType) {
return performanceConfigMapper.selectByObjectIdAndObjectTypeAndYearAndBizType(objectId, objectType, year, bizType);
}
@Override
public List<CrmPerformanceConfigDO> getPerformanceConfigList(Collection<Long> objectIds, Integer objectType,
Integer year, Integer bizType) {
if (isEmpty(objectIds)) {
return Collections.emptyList();
}
return performanceConfigMapper.selectListByObjectIdsAndObjectTypeAndYearAndBizType(objectIds, objectType,
year, bizType);
}
private void fillTargetPrice(CrmPerformanceConfigDO performanceConfig) {
// 1. 空值设置为 0
performanceConfig.setJanuaryTargetPrice(defaultIfNull(performanceConfig.getJanuaryTargetPrice()));
performanceConfig.setFebruaryTargetPrice(defaultIfNull(performanceConfig.getFebruaryTargetPrice()));
performanceConfig.setMarchTargetPrice(defaultIfNull(performanceConfig.getMarchTargetPrice()));
performanceConfig.setAprilTargetPrice(defaultIfNull(performanceConfig.getAprilTargetPrice()));
performanceConfig.setMayTargetPrice(defaultIfNull(performanceConfig.getMayTargetPrice()));
performanceConfig.setJuneTargetPrice(defaultIfNull(performanceConfig.getJuneTargetPrice()));
performanceConfig.setJulyTargetPrice(defaultIfNull(performanceConfig.getJulyTargetPrice()));
performanceConfig.setAugustTargetPrice(defaultIfNull(performanceConfig.getAugustTargetPrice()));
performanceConfig.setSeptemberTargetPrice(defaultIfNull(performanceConfig.getSeptemberTargetPrice()));
performanceConfig.setOctoberTargetPrice(defaultIfNull(performanceConfig.getOctoberTargetPrice()));
performanceConfig.setNovemberTargetPrice(defaultIfNull(performanceConfig.getNovemberTargetPrice()));
performanceConfig.setDecemberTargetPrice(defaultIfNull(performanceConfig.getDecemberTargetPrice()));
// 2. 计算年度目标
performanceConfig.setYearTargetPrice(performanceConfig.getJanuaryTargetPrice()
.add(performanceConfig.getFebruaryTargetPrice())
.add(performanceConfig.getMarchTargetPrice())
.add(performanceConfig.getAprilTargetPrice())
.add(performanceConfig.getMayTargetPrice())
.add(performanceConfig.getJuneTargetPrice())
.add(performanceConfig.getJulyTargetPrice())
.add(performanceConfig.getAugustTargetPrice())
.add(performanceConfig.getSeptemberTargetPrice())
.add(performanceConfig.getOctoberTargetPrice())
.add(performanceConfig.getNovemberTargetPrice())
.add(performanceConfig.getDecemberTargetPrice()));
}
private BigDecimal defaultIfNull(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
}
@@ -1,65 +0,0 @@
package cn.iocoder.yudao.module.crm.service.permission;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmOwnerRecordCreateReqBO;
import jakarta.validation.Valid;
import java.util.List;
/**
* CRM 负责人变更记录 Service 接口
*
* @author 芋道源码
*/
public interface CrmOwnerRecordService {
/**
* 创建负责人变更记录
*
* @param createReqBO 创建信息
* @return 编号
*/
Long createOwnerRecord(@Valid CrmOwnerRecordCreateReqBO createReqBO);
/**
* 批量创建负责人变更记录
*
* @param createReqBOs 创建信息
*/
void createOwnerRecordList(@Valid List<CrmOwnerRecordCreateReqBO> createReqBOs);
/**
* 进入公海客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 进入公海客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO);
}
@@ -1,65 +0,0 @@
package cn.iocoder.yudao.module.crm.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmOwnerRecordDO;
import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmOwnerRecordMapper;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmOwnerRecordCreateReqBO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
* CRM 负责人变更记录 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmOwnerRecordServiceImpl implements CrmOwnerRecordService {
@Resource
private CrmOwnerRecordMapper ownerRecordMapper;
@Override
public Long createOwnerRecord(CrmOwnerRecordCreateReqBO createReqBO) {
CrmOwnerRecordDO ownerRecord = BeanUtils.toBean(createReqBO, CrmOwnerRecordDO.class);
ownerRecordMapper.insert(ownerRecord);
return ownerRecord.getId();
}
@Override
public void createOwnerRecordList(List<CrmOwnerRecordCreateReqBO> createReqBOs) {
if (CollUtil.isEmpty(createReqBOs)) {
return;
}
List<CrmOwnerRecordDO> list = BeanUtils.toBean(createReqBOs, CrmOwnerRecordDO.class);
ownerRecordMapper.insertBatch(list);
}
@Override
public List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerPutCountByDate(reqVO);
}
@Override
public List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerTakeCountByDate(reqVO);
}
@Override
public List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerPutCountByUser(reqVO);
}
@Override
public List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerTakeCountByUser(reqVO);
}
}
@@ -1,39 +0,0 @@
package cn.iocoder.yudao.module.crm.service.permission.bo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* CRM 负责人变更记录 Create Req BO
*
* @author 芋道源码
*/
@Data
public class CrmOwnerRecordCreateReqBO {
/**
* CRM 业务类型
*/
@NotNull(message = "CRM 业务类型不能为空")
@InEnum(CrmBizTypeEnum.class)
private Integer bizType;
/**
* CRM 业务编号
*/
@NotNull(message = "CRM 业务编号不能为空")
private Long bizId;
/**
* 变更前负责人
*/
private Long preOwnerUserId;
/**
* 变更后负责人
*/
private Long postOwnerUserId;
}
@@ -1,23 +0,0 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetRespVO;
import java.util.List;
/**
* CRM 业绩目标统计 Service 接口
*
* @author 芋道源码
*/
public interface CrmStatisticsPerformanceTargetService {
/**
* 获取业绩目标完成情况
*
* @param reqVO 请求参数
* @return 业绩目标完成情况
*/
List<CrmStatisticsPerformanceTargetRespVO> getPerformanceTargetSummary(CrmStatisticsPerformanceTargetReqVO reqVO);
}
@@ -1,190 +0,0 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetActualRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPerformanceTargetMapper;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import cn.iocoder.yudao.module.crm.service.performance.CrmPerformanceConfigService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* CRM 业绩目标统计 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmStatisticsPerformanceTargetServiceImpl implements CrmStatisticsPerformanceTargetService {
@Resource
private CrmStatisticsPerformanceTargetMapper performanceTargetMapper;
@Resource
private CrmPerformanceConfigService performanceConfigService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@Override
public List<CrmStatisticsPerformanceTargetRespVO> getPerformanceTargetSummary(CrmStatisticsPerformanceTargetReqVO reqVO) {
// 1. 获得用户编号数组
reqVO.setUserIds(getUserIds(reqVO));
if (CollUtil.isEmpty(reqVO.getUserIds())) {
return Collections.emptyList();
}
// 2.1 获得目标配置
List<CrmPerformanceConfigDO> performanceConfigs = getPerformanceConfigs(reqVO);
// 2.2 获得实际完成金额
List<CrmStatisticsPerformanceTargetActualRespVO> actualList = ObjUtil.equal(reqVO.getBizType(),
CrmPerformanceConfigBizTypeEnum.CRM_RECEIVABLE.getBizType())
? performanceTargetMapper.selectReceivableActualByMonth(reqVO)
: performanceTargetMapper.selectContractActualByMonth(reqVO);
Map<Integer, BigDecimal> actualMap = convertMap(actualList,
CrmStatisticsPerformanceTargetActualRespVO::getMonth,
CrmStatisticsPerformanceTargetActualRespVO::getCurrentPrice);
// 3. 组装数据返回
List<CrmStatisticsPerformanceTargetRespVO> result = new ArrayList<>();
for (int month = 1; month <= 12; month++) {
BigDecimal targetPrice = getMonthTargetPrice(performanceConfigs, month);
BigDecimal currentPrice = actualMap.getOrDefault(month, BigDecimal.ZERO);
result.add(new CrmStatisticsPerformanceTargetRespVO().setMonth(month).setTargetPrice(targetPrice)
.setCurrentPrice(currentPrice)
.setCompletionRate(calculateCompletionRate(currentPrice, targetPrice)));
}
return result;
}
private List<CrmPerformanceConfigDO> getPerformanceConfigs(CrmStatisticsPerformanceTargetReqVO reqVO) {
// 情况一:选中某个用户
if (ObjUtil.isNotNull(reqVO.getUserId())) {
CrmPerformanceConfigDO performanceConfig = performanceConfigService.getPerformanceConfig(reqVO.getUserId(),
CrmPerformanceConfigObjectTypeEnum.USER.getObjectType(), reqVO.getYear(), reqVO.getBizType());
return performanceConfig == null ? Collections.emptyList() : ListUtil.of(performanceConfig);
}
// 情况二:选中某个部门
return performanceConfigService.getPerformanceConfigList(getDeptIds(reqVO.getDeptId()),
CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType(), reqVO.getYear(), reqVO.getBizType());
}
private BigDecimal getMonthTargetPrice(List<CrmPerformanceConfigDO> performanceConfigs, int month) {
return performanceConfigs.stream()
.map(performanceConfig -> getMonthTargetPrice(performanceConfig, month))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@SuppressWarnings("EnhancedSwitchMigration")
private BigDecimal getMonthTargetPrice(CrmPerformanceConfigDO performanceConfig, int month) {
// 1. 无目标配置,返回 0
if (performanceConfig == null) {
return BigDecimal.ZERO;
}
// 2. 获得月份目标
BigDecimal targetPrice;
switch (month) {
case 1:
targetPrice = performanceConfig.getJanuaryTargetPrice();
break;
case 2:
targetPrice = performanceConfig.getFebruaryTargetPrice();
break;
case 3:
targetPrice = performanceConfig.getMarchTargetPrice();
break;
case 4:
targetPrice = performanceConfig.getAprilTargetPrice();
break;
case 5:
targetPrice = performanceConfig.getMayTargetPrice();
break;
case 6:
targetPrice = performanceConfig.getJuneTargetPrice();
break;
case 7:
targetPrice = performanceConfig.getJulyTargetPrice();
break;
case 8:
targetPrice = performanceConfig.getAugustTargetPrice();
break;
case 9:
targetPrice = performanceConfig.getSeptemberTargetPrice();
break;
case 10:
targetPrice = performanceConfig.getOctoberTargetPrice();
break;
case 11:
targetPrice = performanceConfig.getNovemberTargetPrice();
break;
case 12:
targetPrice = performanceConfig.getDecemberTargetPrice();
break;
default:
targetPrice = BigDecimal.ZERO;
}
return targetPrice != null ? targetPrice : BigDecimal.ZERO;
}
private BigDecimal calculateCompletionRate(BigDecimal currentPrice, BigDecimal targetPrice) {
if (targetPrice == null || targetPrice.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return currentPrice.multiply(BigDecimal.valueOf(100)).divide(targetPrice, 2, RoundingMode.HALF_UP);
}
/**
* 获取用户编号数组
*
* 如果用户编号为空,则获得部门下的用户编号数组,包括子部门的所有用户编号
*
* @param reqVO 请求参数
* @return 用户编号数组
*/
private List<Long> getUserIds(CrmStatisticsPerformanceTargetReqVO reqVO) {
// 情况一:选中某个用户
if (ObjUtil.isNotNull(reqVO.getUserId())) {
return ListUtil.of(reqVO.getUserId());
}
// 情况二:选中某个部门
// 2.1 获得部门列表
List<Long> deptIds = getDeptIds(reqVO.getDeptId());
// 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
}
/**
* 获得部门编号数组,包括子部门
*
* @param deptId 部门编号
* @return 部门编号数组
*/
private List<Long> getDeptIds(Long deptId) {
List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId), DeptRespDTO::getId);
deptIds.add(deptId);
return deptIds;
}
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import java.util.List;
/**
* CRM 产品分析 Service 接口
*
* @author 芋道源码
*/
public interface CrmStatisticsProductService {
/**
* 获取产品销售情况统计
*
* @param reqVO 请求参数
* @return 产品销售情况
*/
List<CrmStatisticsProductSalesRespVO> getProductSalesList(CrmStatisticsProductReqVO reqVO);
/**
* 获取产品分类销售分析
*
* @param reqVO 请求参数
* @return 产品分类销售分析
*/
List<CrmStatisticsProductCategoryRespVO> getProductCategorySummary(CrmStatisticsProductReqVO reqVO);
}
@@ -1,104 +0,0 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.crm.controller.admin.product.vo.category.CrmProductCategoryListReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsProductMapper;
import cn.iocoder.yudao.module.crm.service.product.CrmProductCategoryService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* CRM 产品分析 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmStatisticsProductServiceImpl implements CrmStatisticsProductService {
@Resource
private CrmStatisticsProductMapper productMapper;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@Resource
private CrmProductCategoryService productCategoryService;
@Override
public List<CrmStatisticsProductSalesRespVO> getProductSalesList(CrmStatisticsProductReqVO reqVO) {
reqVO.setUserIds(getUserIds(reqVO));
if (CollUtil.isEmpty(reqVO.getUserIds())) {
return Collections.emptyList();
}
setCategoryIds(reqVO);
return productMapper.selectProductSalesList(reqVO);
}
@Override
public List<CrmStatisticsProductCategoryRespVO> getProductCategorySummary(CrmStatisticsProductReqVO reqVO) {
reqVO.setUserIds(getUserIds(reqVO));
if (CollUtil.isEmpty(reqVO.getUserIds())) {
return Collections.emptyList();
}
setCategoryIds(reqVO);
return productMapper.selectProductCategorySummary(reqVO);
}
/**
* 设置产品分类编号数组,选择父分类时,包含子分类
*
* @param reqVO 请求参数
*/
private void setCategoryIds(CrmStatisticsProductReqVO reqVO) {
if (ObjUtil.isNull(reqVO.getCategoryId())) {
return;
}
List<Long> categoryIds = ListUtil.of(reqVO.getCategoryId());
List<CrmProductCategoryDO> categoryList = productCategoryService.getProductCategoryList(
new CrmProductCategoryListReqVO());
categoryIds.addAll(convertList(categoryList, CrmProductCategoryDO::getId,
category -> Objects.equals(category.getParentId(), reqVO.getCategoryId())));
reqVO.setCategoryIds(categoryIds);
}
/**
* 获取用户编号数组
*
* 如果用户编号为空,则获得部门下的用户编号数组,包括子部门的所有用户编号
*
* @param reqVO 请求参数
* @return 用户编号数组
*/
private List<Long> getUserIds(CrmStatisticsProductReqVO reqVO) {
// 情况一:选中某个用户
if (ObjUtil.isNotNull(reqVO.getUserId())) {
return ListUtil.of(reqVO.getUserId());
}
// 情况二:选中某个部门
// 2.1 获得部门列表
List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
deptIds.add(reqVO.getDeptId());
// 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
}
}
@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmOwnerRecordMapper">
<select id="selectPoolCustomerPutCountByDate"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO">
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
COUNT(*) AS customer_put_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND post_owner_user_id IS NULL
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY time
</select>
<select id="selectPoolCustomerTakeCountByDate"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO">
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
COUNT(*) AS customer_take_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IS NULL
AND post_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY time
</select>
<select id="selectPoolCustomerPutCountByUser"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO">
SELECT pre_owner_user_id AS owner_user_id,
COUNT(*) AS customer_put_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND post_owner_user_id IS NULL
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY pre_owner_user_id
</select>
<select id="selectPoolCustomerTakeCountByUser"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO">
SELECT post_owner_user_id AS owner_user_id,
COUNT(*) AS customer_take_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IS NULL
AND post_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY post_owner_user_id
</select>
</mapper>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPerformanceTargetMapper">
<resultMap id="CrmStatisticsPerformanceTargetActualResultMap"
type="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetActualRespVO">
<result property="month" column="month"/>
<result property="currentPrice" column="current_price"/>
</resultMap>
<select id="selectContractActualByMonth"
resultMap="CrmStatisticsPerformanceTargetActualResultMap">
SELECT MONTH(order_date) AS month,
IFNULL(SUM(total_price), 0) AS current_price
FROM crm_contract
WHERE deleted = 0
AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND YEAR(order_date) = #{year}
GROUP BY MONTH(order_date)
</select>
<select id="selectReceivableActualByMonth"
resultMap="CrmStatisticsPerformanceTargetActualResultMap">
SELECT MONTH(return_time) AS month,
IFNULL(SUM(price), 0) AS current_price
FROM crm_receivable
WHERE deleted = 0
AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND YEAR(return_time) = #{year}
GROUP BY MONTH(return_time)
</select>
</mapper>
@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsProductMapper">
<sql id="ProductStatisticsWhere">
contract.deleted = 0
AND contract_product.deleted = 0
AND product.deleted = 0
AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND contract.owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND contract.order_date BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
<if test="categoryIds != null and categoryIds.size() > 0">
AND product.category_id IN
<foreach collection="categoryIds" item="categoryId" open="(" close=")" separator=",">
#{categoryId}
</foreach>
</if>
<if test="productId != null">
AND product.id = #{productId}
</if>
</sql>
<select id="selectProductSalesList"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO">
SELECT category.id AS category_id,
IFNULL(category.name, '未分类') AS category_name,
product.id AS product_id,
product.name AS product_name,
contract.id AS contract_id,
contract.no AS contract_no,
contract.name AS contract_name,
contract.owner_user_id,
owner.nickname AS owner_user_name,
customer.id AS customer_id,
customer.name AS customer_name,
contract_product.product_price,
contract_product.count AS product_count,
contract_product.total_price AS product_total_price
FROM crm_contract_product AS contract_product
INNER JOIN crm_contract AS contract ON contract_product.contract_id = contract.id
INNER JOIN crm_product AS product ON contract_product.product_id = product.id
LEFT JOIN crm_product_category AS category ON product.category_id = category.id AND category.deleted = 0
LEFT JOIN crm_customer AS customer ON contract.customer_id = customer.id AND customer.deleted = 0
LEFT JOIN system_users AS owner ON contract.owner_user_id = owner.id AND owner.deleted = 0
WHERE <include refid="ProductStatisticsWhere"/>
ORDER BY category.id ASC, product.id ASC, contract.order_date DESC
</select>
<select id="selectProductCategorySummary"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO">
SELECT category.id AS category_id,
IFNULL(category.name, '未分类') AS category_name,
COUNT(DISTINCT contract.id) AS contract_count,
IFNULL(SUM(contract_product.count), 0) AS product_count,
IFNULL(SUM(contract_product.total_price), 0) AS product_total_price
FROM crm_contract_product AS contract_product
INNER JOIN crm_contract AS contract ON contract_product.contract_id = contract.id
INNER JOIN crm_product AS product ON contract_product.product_id = product.id
LEFT JOIN crm_product_category AS category ON product.category_id = category.id AND category.deleted = 0
WHERE <include refid="ProductStatisticsWhere"/>
GROUP BY category.id, category.name
ORDER BY product_count DESC, product_total_price DESC
</select>
</mapper>
@@ -1,422 +0,0 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link CodegenEngine} 的 Vue3 + Vben5 + Antdv Next 单元测试
*
* @author 芋道源码
*/
public class CodegenEngineVben5AntdvNextTest extends CodegenEngineAbstractTest {
private static final Pattern ANTDV_NEXT_VALUE_IMPORT_PATTERN = Pattern.compile(
"import\\s*\\{([^}]*)}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
private static final Pattern RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile("<RangePicker\\b");
private static final Pattern RANGE_PICKER_HELPER_BINDING_PATTERN = Pattern.compile(
"\\bv-bind\\s*=\\s*([\"'])\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)\\s*\\1");
private static final Pattern RANGE_PICKER_HELPER_CALL_PATTERN = Pattern.compile(
"\\bgetRangePickerDefaultProps\\s*\\(\\s*\\)");
private static final Pattern RANGE_PICKER_HELPER_IMPORT_PATTERN = Pattern.compile(
"import\\s*\\{[^}]*\\bgetRangePickerDefaultProps\\b[^}]*}"
+ "\\s*from\\s*'#/utils(?:/rangePickerProps)?';?", Pattern.DOTALL);
private static final Pattern SCHEMA_RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile(
"\\bcomponent\\s*:\\s*([\"'])RangePicker\\1");
private static final Pattern SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN = Pattern.compile(
"\\.\\.\\.\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)");
private static final String IMPORT_FORM_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/modules/import-form.vue";
private static final String SCHEMA_DATA_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/data.ts";
private static final String STUDENT_CONTACT_FORM_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue";
private static final String STUDENT_TEACHER_FORM_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue";
private static final List<String> INNER_TEMPLATE_PATHS = Arrays.asList(
"codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_inner.vue.vm",
"codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_inner.vue.vm",
"codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_inner.vue.vm",
"codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_inner.vue.vm");
@Test
public void testExecute_schemaMasterNormal() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
CodegenTemplateTypeEnum.MASTER_NORMAL);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertSchemaRangePickerGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_schemaMasterErp() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
CodegenTemplateTypeEnum.MASTER_ERP);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertSchemaRangePickerGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_schemaMasterInner() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
CodegenTemplateTypeEnum.MASTER_INNER);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertSchemaRangePickerGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_generalTree() {
// 调用
Map<String, String> result = executeTree(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType());
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertComponentGenerated(result, "FormItem");
assertComponentGenerated(result, "TreeSelect");
}
@Test
public void testExecute_generalMasterNormal() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
CodegenTemplateTypeEnum.MASTER_NORMAL);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertGeneralCrudComponentsGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_generalMasterNormal_oneToOneJoinColumnOnlySelect() {
// 调用
Map<String, String> result = executeGeneralMasterNormalWithOneToOneJoinSelect();
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
String content = result.get(STUDENT_TEACHER_FORM_PATH);
assertNotNull(content, STUDENT_TEACHER_FORM_PATH + ":未生成一对一子表表单");
assertTrue(content.contains("<Select v-model:value=\"formData.studentId\""),
STUDENT_TEACHER_FORM_PATH + ":应渲染 join 列的 Select 组件");
assertTrue(content.contains("<SelectOption"), STUDENT_TEACHER_FORM_PATH + ":应渲染 SelectOption 组件");
assertRootValueImport(STUDENT_TEACHER_FORM_PATH, content, "\\bSelectOption\\b");
}
@Test
public void testExecute_generalMasterNormal_oneToManyJoinColumnOnlySelect() {
// 调用
Map<String, String> result = executeGeneralMasterNormalWithOneToManyJoinSelect();
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
String content = result.get(STUDENT_CONTACT_FORM_PATH);
assertNotNull(content, STUDENT_CONTACT_FORM_PATH + ":未生成一对多子表表单");
assertTrue(content.contains("<VxeTable"), STUDENT_CONTACT_FORM_PATH + ":应生成一对多子表表格");
assertFalse(content.contains("<VxeColumn field=\"studentId\""),
STUDENT_CONTACT_FORM_PATH + ":不应生成 join 列");
assertFalse(content.contains("<Select v-model:value=\"row.studentId\""),
STUDENT_CONTACT_FORM_PATH + ":不应渲染 join 列的 Select 组件");
assertFalse(content.contains("<SelectOption"), STUDENT_CONTACT_FORM_PATH + ":不应渲染 SelectOption 组件");
assertFalse(createRootImportPattern("import\\s*\\{", "\\bSelectOption\\b").matcher(content).find(),
STUDENT_CONTACT_FORM_PATH + ":不应导入 SelectOption 组件");
}
@Test
public void testExecute_generalMasterInner() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
CodegenTemplateTypeEnum.MASTER_INNER);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertGeneralCrudComponentsGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_generalMasterErp() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
CodegenTemplateTypeEnum.MASTER_ERP);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertGeneralCrudComponentsGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_oneImportEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 调用并断言
assertImportFormCompatibleWithAntdvNext(
executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType()));
assertImportFormCompatibleWithAntdvNext(
executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType()));
}
@Test
public void testInnerTemplatesParseAntdvNext() {
INNER_TEMPLATE_PATHS.forEach(path -> {
String template = ResourceUtil.readUtf8Str(path);
assertFalse(template.contains("codegen/vue3_vben5_antd/"), path + ":不应引用 Ant Design Vue 模板");
assertTrue(template.contains("codegen/vue3_vben5_antdv_next/"), path + ":应引用 Antdv Next 模板");
});
}
private Map<String, String> executeOne(Integer frontType) {
CodegenTableDO table = getTable("student")
.setFrontType(frontType)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
}
private Map<String, String> executeTree(Integer frontType) {
CodegenTableDO table = getTable("category")
.setFrontType(frontType)
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("category"), null, null);
}
private Map<String, String> executeGeneralMasterNormalWithOneToOneJoinSelect() {
return executeGeneralMasterNormalWithJoinSelect("teacher", 200L, false);
}
private Map<String, String> executeGeneralMasterNormalWithOneToManyJoinSelect() {
return executeGeneralMasterNormalWithJoinSelect("contact", 100L, true);
}
private Map<String, String> executeGeneralMasterNormalWithJoinSelect(String subTableName,
Long subJoinColumnId,
boolean subJoinMany) {
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
CodegenTableDO subTable = getTable(subTableName)
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
.setSubJoinColumnId(subJoinColumnId).setSubJoinMany(subJoinMany);
List<CodegenColumnDO> subColumns = getColumnList(subTableName);
// 稀疏场景:join 列是唯一的 select,避免其它 select 掩盖 join 列对导入判定的影响。
subColumns.forEach(column -> {
if (subJoinColumnId.equals(column.getId())) {
column.setHtmlType("select");
column.setDictType("");
} else if ("select".equals(column.getHtmlType())) {
column.setHtmlType("input");
}
});
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"),
List.of(subTable), List.of(subColumns));
}
private Map<String, String> executeMaster(Integer frontType, CodegenTemplateTypeEnum templateType) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(frontType)
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(frontType)
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(frontType)
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
return codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
}
private static void assertGeneratedCodeCompatibleWithAntdvNext(Map<String, String> result) {
assertTrue(result.values().stream().anyMatch(content -> content.contains("from 'antdv-next")),
"生成结果应使用 antdv-next");
result.forEach((filePath, content) -> {
assertFalse(content.contains("ant-design-vue"), filePath + ":不应使用 ant-design-vue");
assertFalse(content.contains("antdv-next/es/"), filePath + ":不应使用未导出的 antdv-next 深层路径");
assertFalse(content.contains("<Form.Item"), filePath + ":不应使用 Form.Item");
assertFalse(content.contains("</Form.Item>"), filePath + ":不应使用 Form.Item");
assertRootValueImportsSorted(filePath, content);
assertRootValueImportMatchesUsage(filePath, content,
"\\bDateRangePicker\\s+as\\s+RangePicker\\b", "<RangePicker\\b", "RangePicker");
assertRootValueImportMatchesUsage(filePath, content,
"\\bFormItem\\b", "<FormItem\\b", "FormItem");
assertRootValueImportMatchesUsage(filePath, content,
"\\bSelectOption\\b", "<SelectOption\\b", "SelectOption");
assertRootValueImportMatchesUsage(filePath, content,
"\\bTabPane\\b", "<TabPane\\b", "TabPane");
assertRootValueImportMatchesUsage(filePath, content,
"\\bTabs\\b", "<Tabs\\b", "Tabs");
assertRootTypeImportMatchesUsage(filePath, content,
"\\bRule\\b", "\\bRule\\b", "Rule");
assertRangePickerHelperImportMatchesUsage(filePath, content);
if (filePath.endsWith(".vue")) {
assertRangePickerHelperBindingMatchesComponent(filePath, content);
} else if (filePath.endsWith(".ts")) {
assertSchemaRangePickerHelperMatchesComponent(filePath, content);
}
});
}
private static void assertImportFormCompatibleWithAntdvNext(Map<String, String> result) {
assertGeneratedCodeCompatibleWithAntdvNext(result);
String content = result.get(IMPORT_FORM_PATH);
assertNotNull(content, IMPORT_FORM_PATH + ":未生成导入表单");
assertTrue(content.contains("<Upload"), IMPORT_FORM_PATH + ":应生成 Upload 组件");
assertRootValueImport(IMPORT_FORM_PATH, content, "\\bUpload\\b");
assertTrue(content.contains("beforeUpload(file: File)"), IMPORT_FORM_PATH + ":上传参数应使用原生 File 类型");
assertFalse(content.contains("UploadFile"), IMPORT_FORM_PATH + ":不应使用上传列表项类型 UploadFile");
assertFalse(content.contains("as unknown as File"), IMPORT_FORM_PATH + ":不应通过双重断言转换上传文件");
assertFalse(content.contains("FileType"), IMPORT_FORM_PATH + ":不应继续使用旧 FileType");
}
private static void assertTabsGenerated(Map<String, String> result) {
assertComponentGenerated(result, "Tabs");
assertComponentGenerated(result, "TabPane");
}
private static void assertGeneralCrudComponentsGenerated(Map<String, String> result) {
assertComponentGenerated(result, "FormItem");
assertComponentGenerated(result, "RangePicker");
assertComponentGenerated(result, "SelectOption");
}
private static void assertRuleGenerated(Map<String, String> result) {
assertTrue(result.values().stream().map(CodegenEngineVben5AntdvNextTest::removeAntdvNextRootImports)
.anyMatch(content -> Pattern.compile("\\bRule\\b").matcher(content).find()),
"生成结果应包含 Rule 类型校验规则");
}
private static void assertSchemaRangePickerGenerated(Map<String, String> result) {
String content = result.get(SCHEMA_DATA_PATH);
assertNotNull(content, SCHEMA_DATA_PATH + ":未生成 Schema 配置");
long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
assertTrue(componentCount > 0, SCHEMA_DATA_PATH + ":应生成 RangePicker 组件配置");
}
private static void assertComponentGenerated(Map<String, String> result, String component) {
assertTrue(result.values().stream().anyMatch(content -> content.contains("<" + component)),
"生成结果应包含 " + component + " 组件");
}
private static void assertRootValueImportMatchesUsage(String filePath, String content,
String expectedImportRegex, String usageRegex,
String bindingName) {
assertRootImportMatchesUsage(filePath, content, "import\\s*\\{", expectedImportRegex,
usageRegex, bindingName);
}
private static void assertRootTypeImportMatchesUsage(String filePath, String content,
String expectedImportRegex, String usageRegex,
String bindingName) {
assertRootImportMatchesUsage(filePath, content, "import\\s+type\\s*\\{", expectedImportRegex,
usageRegex, bindingName);
}
private static void assertRootImportMatchesUsage(String filePath, String content, String importPrefix,
String expectedImportRegex, String usageRegex,
String bindingName) {
boolean imported = createRootImportPattern(importPrefix, expectedImportRegex).matcher(content).find();
boolean used = Pattern.compile(usageRegex).matcher(removeAntdvNextRootImports(content)).find();
assertEquals(used, imported, filePath + ":" + bindingName + (used ? " 缺少合法根导入" : " 存在孤立根导入"));
}
private static String removeAntdvNextRootImports(String content) {
Pattern pattern = Pattern.compile("import\\s+(?:type\\s+)?\\{[^}]*}\\s*from\\s*'antdv-next';?",
Pattern.DOTALL);
return pattern.matcher(content).replaceAll("");
}
private static void assertRootValueImport(String filePath, String content, String expectedRegex) {
assertRootImport(filePath, content, "import\\s*\\{", expectedRegex);
}
private static void assertRootValueImportsSorted(String filePath, String content) {
var matcher = ANTDV_NEXT_VALUE_IMPORT_PATTERN.matcher(content);
while (matcher.find()) {
List<String> actual = Arrays.stream(matcher.group(1).split(","))
.map(String::trim)
.filter(item -> !item.isEmpty())
.map(item -> item.replaceFirst("^.*\\s+as\\s+", ""))
.toList();
List<String> expected = actual.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList();
assertEquals(expected, actual, filePath + ":antdv-next 命名导入未按 natural 顺序排列");
}
}
private static void assertRangePickerHelperImportMatchesUsage(String filePath, String content) {
boolean imported = RANGE_PICKER_HELPER_IMPORT_PATTERN.matcher(content).find();
boolean called = RANGE_PICKER_HELPER_CALL_PATTERN.matcher(content).find();
assertEquals(called, imported,
filePath + ":getRangePickerDefaultProps() 调用与导入不一致");
}
private static void assertRangePickerHelperBindingMatchesComponent(String filePath, String content) {
long componentCount = countMatches(RANGE_PICKER_COMPONENT_PATTERN, content);
long bindingCount = countMatches(RANGE_PICKER_HELPER_BINDING_PATTERN, content);
assertEquals(componentCount, bindingCount,
filePath + ":RangePicker 与 getRangePickerDefaultProps() 绑定数量不一致");
}
private static void assertSchemaRangePickerHelperMatchesComponent(String filePath, String content) {
long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
long helperSpreadCount = countMatches(SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN, content);
assertEquals(componentCount, helperSpreadCount,
filePath + ":Schema RangePicker 与 getRangePickerDefaultProps() 展开数量不一致");
}
private static long countMatches(Pattern pattern, String content) {
return pattern.matcher(content).results().count();
}
private static void assertRootImport(String filePath, String content, String importPrefix, String expectedRegex) {
Pattern pattern = createRootImportPattern(importPrefix, expectedRegex);
assertTrue(pattern.matcher(content).find(), filePath + ":缺少合法的 antdv-next 根导入 " + expectedRegex);
}
private static Pattern createRootImportPattern(String importPrefix, String expectedRegex) {
return Pattern.compile(importPrefix + "[^}]*" + expectedRegex
+ "[^}]*}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
}
}
@@ -1,73 +0,0 @@
[ {
"contentPath" : "java/InfraStudentPageReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
}, {
"contentPath" : "java/InfraStudentRespVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
}, {
"contentPath" : "java/InfraStudentSaveReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
}, {
"contentPath" : "java/InfraStudentController",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
}, {
"contentPath" : "java/InfraStudentDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
}, {
"contentPath" : "java/InfraStudentContactDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
}, {
"contentPath" : "java/InfraStudentTeacherDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
}, {
"contentPath" : "java/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
}, {
"contentPath" : "java/InfraStudentContactMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
}, {
"contentPath" : "java/InfraStudentTeacherMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
}, {
"contentPath" : "xml/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
}, {
"contentPath" : "java/InfraStudentServiceImpl",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
}, {
"contentPath" : "java/InfraStudentService",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
}, {
"contentPath" : "java/InfraStudentServiceImplTest",
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
}, {
"contentPath" : "java/ErrorCodeConstants_手动操作",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
}, {
"contentPath" : "sql/sql",
"filePath" : "sql/sql.sql"
}, {
"contentPath" : "sql/h2",
"filePath" : "sql/h2.sql"
}, {
"contentPath" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
}, {
"contentPath" : "vue/student-contact/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue"
}, {
"contentPath" : "vue/student-teacher/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-teacher/form/index.vue"
} ]
@@ -1,6 +0,0 @@
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
// ========== 学生 TODO 补充编号 ==========
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
@@ -1,71 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生联系人 DO
*
* @author 芋道源码
*/
@TableName("infra_student_contact")
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentContactDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 学生编号
*/
private Long studentId;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,34 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 学生联系人 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
.eq(InfraStudentContactDO::getStudentId, studentId)
.orderByDesc(InfraStudentContactDO::getId));
}
default int deleteByStudentId(Long studentId) {
return delete(InfraStudentContactDO::getStudentId, studentId);
}
default int deleteByStudentIds(List<Long> studentIds) {
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
}
}
@@ -1,210 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.*;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
@Tag(name = "管理后台 - 学生")
@RestController
@RequestMapping("/infra/student")
@Validated
public class InfraStudentController {
@Resource
private InfraStudentService studentService;
@PostMapping("/create")
@Operation(summary = "创建学生")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
return success(studentService.createStudent(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新学生")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
studentService.updateStudent(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除学生")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
studentService.deleteStudent(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得学生")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
InfraStudentDO student = studentService.getStudent(id);
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得学生分页")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出学生 Excel")
@PreAuthorize("@ss.hasPermission('infra:student:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
BeanUtils.toBean(list, InfraStudentRespVO.class));
}
// ==================== 子表(学生联系人) ====================
@GetMapping("/student-contact/page")
@Operation(summary = "获得学生联系人分页")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentContactPage(pageReqVO, studentId));
}
@PostMapping("/student-contact/create")
@Operation(summary = "创建学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
return success(studentService.createStudentContact(studentContact));
}
@PutMapping("/student-contact/update")
@Operation(summary = "更新学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
studentService.updateStudentContact(studentContact);
return success(true);
}
@DeleteMapping("/student-contact/delete")
@Parameter(name = "id", description = "编号", required = true)
@Operation(summary = "删除学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
studentService.deleteStudentContact(id);
return success(true);
}
@DeleteMapping("/student-contact/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentContactListByIds(ids);
return success(true);
}
@GetMapping("/student-contact/get")
@Operation(summary = "获得学生联系人")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
return success(studentService.getStudentContact(id));
}
// ==================== 子表(学生班主任) ====================
@GetMapping("/student-teacher/page")
@Operation(summary = "获得学生班主任分页")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
}
@PostMapping("/student-teacher/create")
@Operation(summary = "创建学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
return success(studentService.createStudentTeacher(studentTeacher));
}
@PutMapping("/student-teacher/update")
@Operation(summary = "更新学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
studentService.updateStudentTeacher(studentTeacher);
return success(true);
}
@DeleteMapping("/student-teacher/delete")
@Parameter(name = "id", description = "编号", required = true)
@Operation(summary = "删除学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
studentService.deleteStudentTeacher(id);
return success(true);
}
@DeleteMapping("/student-teacher/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentTeacherListByIds(ids);
return success(true);
}
@GetMapping("/student-teacher/get")
@Operation(summary = "获得学生班主任")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
return success(studentService.getStudentTeacher(id));
}
}
@@ -1,68 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生 DO
*
* @author 芋道源码
*/
@TableName("infra_student")
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,30 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
/**
* 学生 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(InfraStudentDO::getId));
}
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 学生分页 Request VO")
@Data
public class InfraStudentPageReqVO extends PageParam {
@Schema(description = "名字", example = "芋头")
private String name;
@Schema(description = "出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", example = "1")
private Integer sex;
@Schema(description = "是否有效", example = "true")
private Boolean enabled;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -1,59 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.idev.excel.annotation.*;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
@Schema(description = "管理后台 - 学生 Response VO")
@Data
@ExcelIgnoreUnannotated
public class InfraStudentRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("编号")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@ExcelProperty("名字")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@ExcelProperty("简介")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "性别", converter = DictConvert.class)
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@ExcelProperty("头像")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@ExcelProperty("附件")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@ExcelProperty("备注")
private String memo;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}
@@ -1,51 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
@Data
public class InfraStudentSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@NotEmpty(message = "名字不能为空")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@NotEmpty(message = "简介不能为空")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "出生日期不能为空")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "性别不能为空")
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否有效不能为空")
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@NotEmpty(message = "头像不能为空")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@NotEmpty(message = "附件不能为空")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@NotEmpty(message = "备注不能为空")
private String memo;
}
@@ -1,160 +0,0 @@
package cn.iocoder.yudao.module.infra.service.demo;
import java.util.*;
import jakarta.validation.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* 学生 Service 接口
*
* @author 芋道源码
*/
public interface InfraStudentService {
/**
* 创建学生
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
/**
* 更新学生
*
* @param updateReqVO 更新信息
*/
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
/**
* 删除学生
*
* @param id 编号
*/
void deleteStudent(Long id);
/**
* 批量删除学生
*
* @param ids 编号
*/
void deleteStudentListByIds(List<Long> ids);
/**
* 获得学生
*
* @param id 编号
* @return 学生
*/
InfraStudentDO getStudent(Long id);
/**
* 获得学生分页
*
* @param pageReqVO 分页查询
* @return 学生分页
*/
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
// ==================== 子表(学生联系人) ====================
/**
* 获得学生联系人分页
*
* @param pageReqVO 分页查询
* @param studentId 学生编号
* @return 学生联系人分页
*/
PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
/**
* 创建学生联系人
*
* @param studentContact 创建信息
* @return 编号
*/
Long createStudentContact(@Valid InfraStudentContactDO studentContact);
/**
* 更新学生联系人
*
* @param studentContact 更新信息
*/
void updateStudentContact(@Valid InfraStudentContactDO studentContact);
/**
* 删除学生联系人
*
* @param id 编号
*/
void deleteStudentContact(Long id);
/**
* 批量删除学生联系人
*
* @param ids 编号
*/
void deleteStudentContactListByIds(List<Long> ids);
/**
* 获得学生联系人
*
* @param id 编号
* @return 学生联系人
*/
InfraStudentContactDO getStudentContact(Long id);
// ==================== 子表(学生班主任) ====================
/**
* 获得学生班主任分页
*
* @param pageReqVO 分页查询
* @param studentId 学生编号
* @return 学生班主任分页
*/
PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
/**
* 创建学生班主任
*
* @param studentTeacher 创建信息
* @return 编号
*/
Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
/**
* 更新学生班主任
*
* @param studentTeacher 更新信息
*/
void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
/**
* 删除学生班主任
*
* @param id 编号
*/
void deleteStudentTeacher(Long id);
/**
* 批量删除学生班主任
*
* @param ids 编号
*/
void deleteStudentTeacherListByIds(List<Long> ids);
/**
* 获得学生班主任
*
* @param id 编号
* @return 学生班主任
*/
InfraStudentTeacherDO getStudentTeacher(Long id);
}
@@ -1,216 +0,0 @@
package cn.iocoder.yudao.module.infra.service.demo;
import cn.hutool.core.collection.CollUtil;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
/**
* 学生 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class InfraStudentServiceImpl implements InfraStudentService {
@Resource
private InfraStudentMapper studentMapper;
@Resource
private InfraStudentContactMapper studentContactMapper;
@Resource
private InfraStudentTeacherMapper studentTeacherMapper;
@Override
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
// 插入
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
studentMapper.insert(student);
// 返回
return student.getId();
}
@Override
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
// 校验存在
validateStudentExists(updateReqVO.getId());
// 更新
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
studentMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStudent(Long id) {
// 校验存在
validateStudentExists(id);
// 删除
studentMapper.deleteById(id);
// 删除子表
deleteStudentContactByStudentId(id);
deleteStudentTeacherByStudentId(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStudentListByIds(List<Long> ids) {
// 删除
studentMapper.deleteByIds(ids);
// 删除子表
deleteStudentContactByStudentIds(ids);
deleteStudentTeacherByStudentIds(ids);
}
private void validateStudentExists(Long id) {
if (studentMapper.selectById(id) == null) {
throw exception(STUDENT_NOT_EXISTS);
}
}
@Override
public InfraStudentDO getStudent(Long id) {
return studentMapper.selectById(id);
}
@Override
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
return studentMapper.selectPage(pageReqVO);
}
// ==================== 子表(学生联系人) ====================
@Override
public PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId) {
return studentContactMapper.selectPage(pageReqVO, studentId);
}
@Override
public Long createStudentContact(InfraStudentContactDO studentContact) {
studentContact.clean(); // 清理掉创建、更新时间等相关属性值
studentContactMapper.insert(studentContact);
return studentContact.getId();
}
@Override
public void updateStudentContact(InfraStudentContactDO studentContact) {
// 校验存在
validateStudentContactExists(studentContact.getId());
// 更新
studentContact.clean(); // 解决更新情况下:updateTime 不更新
studentContactMapper.updateById(studentContact);
}
@Override
public void deleteStudentContact(Long id) {
// 删除
studentContactMapper.deleteById(id);
}
@Override
public void deleteStudentContactListByIds(List<Long> ids) {
// 删除
studentContactMapper.deleteByIds(ids);
}
@Override
public InfraStudentContactDO getStudentContact(Long id) {
return studentContactMapper.selectById(id);
}
private void validateStudentContactExists(Long id) {
if (studentContactMapper.selectById(id) == null) {
throw exception(STUDENT_CONTACT_NOT_EXISTS);
}
}
private void deleteStudentContactByStudentId(Long studentId) {
studentContactMapper.deleteByStudentId(studentId);
}
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
studentContactMapper.deleteByStudentIds(studentIds);
}
// ==================== 子表(学生班主任) ====================
@Override
public PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId) {
return studentTeacherMapper.selectPage(pageReqVO, studentId);
}
@Override
public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) {
// 校验是否已经存在
if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) {
throw exception(STUDENT_TEACHER_EXISTS);
}
// 插入
studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值
studentTeacherMapper.insert(studentTeacher);
return studentTeacher.getId();
}
@Override
public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) {
// 校验存在
validateStudentTeacherExists(studentTeacher.getId());
// 更新
studentTeacher.clean(); // 解决更新情况下:updateTime 不更新
studentTeacherMapper.updateById(studentTeacher);
}
@Override
public void deleteStudentTeacher(Long id) {
// 删除
studentTeacherMapper.deleteById(id);
}
@Override
public void deleteStudentTeacherListByIds(List<Long> ids) {
// 删除
studentTeacherMapper.deleteByIds(ids);
}
@Override
public InfraStudentTeacherDO getStudentTeacher(Long id) {
return studentTeacherMapper.selectById(id);
}
private void validateStudentTeacherExists(Long id) {
if (studentTeacherMapper.selectById(id) == null) {
throw exception(STUDENT_TEACHER_NOT_EXISTS);
}
}
private void deleteStudentTeacherByStudentId(Long studentId) {
studentTeacherMapper.deleteByStudentId(studentId);
}
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
studentTeacherMapper.deleteByStudentIds(studentIds);
}
}
@@ -1,145 +0,0 @@
package cn.iocoder.yudao.module.infra.service.demo;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Import;
import java.util.*;
import java.time.LocalDateTime;
import static cn.hutool.core.util.RandomUtil.*;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link InfraStudentServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(InfraStudentServiceImpl.class)
public class InfraStudentServiceImplTest extends BaseDbUnitTest {
@Resource
private InfraStudentServiceImpl studentService;
@Resource
private InfraStudentMapper studentMapper;
@Test
public void testCreateStudent_success() {
// 准备参数
InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
// 调用
Long studentId = studentService.createStudent(createReqVO);
// 断言
assertNotNull(studentId);
// 校验记录的属性是否正确
InfraStudentDO student = studentMapper.selectById(studentId);
assertPojoEquals(createReqVO, student, "id");
}
@Test
public void testUpdateStudent_success() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
// 准备参数
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
o.setId(dbStudent.getId()); // 设置更新的 ID
});
// 调用
studentService.updateStudent(updateReqVO);
// 校验是否更新正确
InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
assertPojoEquals(updateReqVO, student);
}
@Test
public void testUpdateStudent_notExists() {
// 准备参数
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
}
@Test
public void testDeleteStudent_success() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbStudent.getId();
// 调用
studentService.deleteStudent(id);
// 校验数据不存在了
assertNull(studentMapper.selectById(id));
}
@Test
public void testDeleteStudent_notExists() {
// 准备参数
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
}
@Test
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
public void testGetStudentPage() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
o.setName(null);
o.setBirthday(null);
o.setSex(null);
o.setEnabled(null);
o.setCreateTime(null);
});
studentMapper.insert(dbStudent);
// 测试 name 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
// 测试 birthday 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
// 测试 sex 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
// 测试 enabled 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
// 测试 createTime 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
// 准备参数
InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
reqVO.setName(null);
reqVO.setBirthday(null);
reqVO.setSex(null);
reqVO.setEnabled(null);
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
// 调用
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbStudent, pageResult.getList().get(0));
}
}
@@ -1,71 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生班主任 DO
*
* @author 芋道源码
*/
@TableName("infra_student_teacher")
@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentTeacherDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 学生编号
*/
private Long studentId;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,37 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 学生班主任 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
default PageResult<InfraStudentTeacherDO> selectPage(PageParam reqVO, Long studentId) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentTeacherDO>()
.eq(InfraStudentTeacherDO::getStudentId, studentId)
.orderByDesc(InfraStudentTeacherDO::getId));
}
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
}
default int deleteByStudentId(Long studentId) {
return delete(InfraStudentTeacherDO::getStudentId, studentId);
}
default int deleteByStudentIds(List<Long> studentIds) {
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
}
}
@@ -1,17 +0,0 @@
-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
CREATE TABLE IF NOT EXISTS "infra_student" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar NOT NULL,
"description" varchar NOT NULL,
"birthday" varchar NOT NULL,
"sex" int NOT NULL,
"enabled" bit NOT NULL,
"avatar" varchar NOT NULL,
"video" varchar NOT NULL,
"memo" varchar NOT NULL,
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("id")
) COMMENT '学生表';
-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
DELETE FROM "infra_student";
@@ -1,54 +0,0 @@
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'学生管理', '', 2, 0, 888,
'student', '', 'infra/demo/index', 0, 'InfraStudent'
);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生查询', 'infra:student:query', 3, 1, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生创建', 'infra:student:create', 3, 2, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生更新', 'infra:student:update', 3, 3, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生删除', 'infra:student:delete', 3, 4, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生导出', 'infra:student:export', 3, 5, @parentId,
'', '', '', 0
);
@@ -1,125 +0,0 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
/** 学生联系人信息 */
export interface StudentContact {
id?: number
studentId: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生班主任信息 */
export interface StudentTeacher {
id?: number
studentId: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生信息 */
export interface Student {
id?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video: string
memo: string
createTime?: number
}
/** 获取学生分页列表 */
export function getStudentPage(params: PageParam) {
return http.get<PageResult<Student>>('/infra/student/page', params)
}
/** 获取学生详情 */
export function getStudent(id: number) {
return http.get<Student>(`/infra/student/get?id=${id}`)
}
/** 创建学生 */
export function createStudent(data: Student) {
return http.post<number>('/infra/student/create', data)
}
/** 更新学生 */
export function updateStudent(data: Student) {
return http.put<boolean>('/infra/student/update', data)
}
/** 删除学生 */
export function deleteStudent(id: number) {
return http.delete<boolean>(`/infra/student/delete?id=${id}`)
}
// ==================== 子表:学生联系人 ====================
/** 获取学生联系人分页列表 */
export function getStudentContactPage(params: PageParam & { studentId: number }) {
return http.get<PageResult<StudentContact>>('/infra/student/student-contact/page', params)
}
/** 获取学生联系人详情 */
export function getStudentContact(id: number) {
return http.get<StudentContact>(`/infra/student/student-contact/get?id=${id}`)
}
/** 创建学生联系人 */
export function createStudentContact(data: StudentContact) {
return http.post<number>('/infra/student/student-contact/create', data)
}
/** 更新学生联系人 */
export function updateStudentContact(data: StudentContact) {
return http.put<boolean>('/infra/student/student-contact/update', data)
}
/** 删除学生联系人 */
export function deleteStudentContact(id: number) {
return http.delete<boolean>(`/infra/student/student-contact/delete?id=${id}`)
}
// ==================== 子表:学生班主任 ====================
/** 获取学生班主任分页列表 */
export function getStudentTeacherPage(params: PageParam & { studentId: number }) {
return http.get<PageResult<StudentTeacher>>('/infra/student/student-teacher/page', params)
}
/** 获取学生班主任详情 */
export function getStudentTeacher(id: number) {
return http.get<StudentTeacher>(`/infra/student/student-teacher/get?id=${id}`)
}
/** 创建学生班主任 */
export function createStudentTeacher(data: StudentTeacher) {
return http.post<number>('/infra/student/student-teacher/create', data)
}
/** 更新学生班主任 */
export function updateStudentTeacher(data: StudentTeacher) {
return http.put<boolean>('/infra/student/student-teacher/update', data)
}
/** 删除学生班主任 */
export function deleteStudentTeacher(id: number) {
return http.delete<boolean>(`/infra/student/student-teacher/delete?id=${id}`)
}
@@ -1,342 +0,0 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar title="学生详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="学生信息">
<wd-cell title="编号" :value="formData?.id ?? '-'" />
<wd-cell title="名字" :value="formData?.name ?? '-'" />
<wd-cell title="简介" :value="formData?.description ?? '-'" />
<wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
<wd-cell title="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
</wd-cell>
<wd-cell title="是否有效">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
</wd-cell>
<wd-cell title="头像" :value="formData?.avatar ?? '-'" />
<wd-cell title="附件" :value="formData?.video ?? '-'" />
<wd-cell title="备注" :value="formData?.memo ?? '-'" />
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
</wd-cell-group>
<!-- 子表:学生联系人 -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">学生联系人({{ studentContactTotal }})</text>
<wd-button
v-if="hasAccessByCodes(['infra:student:create'])"
size="small" type="primary" variant="plain" @click="openStudentContact()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<z-paging
ref="studentContactPagingRef"
v-model="studentContactList"
:fixed="false"
:auto="false"
height="560rpx"
:default-page-size="10"
:refresher-enabled="false"
:inside-more="true"
:to-bottom-loading-more-enabled="false"
loading-more-default-text="点击加载更多"
empty-view-text="暂无学生联系人"
@query="queryStudentContactList"
>
<view
v-for="item in studentContactList"
:key="item.id"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-8rpx text-28rpx text-[#666]">
学生编号:{{ item.studentId ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
名字:{{ item.name ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
简介:{{ item.description ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
出生日期:{{ item.birthday ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
性别:{{ item.sex ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
是否有效:{{ item.enabled ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
头像:{{ item.avatar ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
附件:{{ item.video ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
备注:{{ item.memo ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
创建时间:{{ item.createTime ?? '-' }}
</view>
<view class="mt-16rpx flex justify-end gap-16rpx">
<wd-button size="small" variant="plain" @click="openStudentContact(item)">
编辑
</wd-button>
<wd-button size="small" type="danger" variant="plain" @click="removeStudentContact(item)">
删除
</wd-button>
</view>
</view>
</z-paging>
</view>
<!-- 子表:学生班主任 -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">学生班主任({{ studentTeacherTotal }})</text>
<wd-button
v-if="studentTeacherTotal === 0 && hasAccessByCodes(['infra:student:create'])"
size="small" type="primary" variant="plain" @click="openStudentTeacher()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<z-paging
ref="studentTeacherPagingRef"
v-model="studentTeacherList"
:fixed="false"
:auto="false"
height="560rpx"
:default-page-size="1"
:refresher-enabled="false"
:inside-more="true"
:to-bottom-loading-more-enabled="false"
loading-more-default-text="点击加载更多"
empty-view-text="暂无学生班主任"
@query="queryStudentTeacherList"
>
<view
v-for="item in studentTeacherList"
:key="item.id"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-8rpx text-28rpx text-[#666]">
学生编号:{{ item.studentId ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
名字:{{ item.name ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
简介:{{ item.description ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
出生日期:{{ item.birthday ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
性别:{{ item.sex ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
是否有效:{{ item.enabled ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
头像:{{ item.avatar ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
附件:{{ item.video ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
备注:{{ item.memo ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
创建时间:{{ item.createTime ?? '-' }}
</view>
<view class="mt-16rpx flex justify-end gap-16rpx">
<wd-button size="small" variant="plain" @click="openStudentTeacher(item)">
编辑
</wd-button>
<wd-button size="small" type="danger" variant="plain" @click="removeStudentTeacher(item)">
删除
</wd-button>
</view>
</view>
</z-paging>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button v-if="hasAccessByCodes(['infra:student:update'])" class="flex-1" type="warning" @click="handleEdit">
编辑
</wd-button>
<wd-button v-if="hasAccessByCodes(['infra:student:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import type {
Student,
StudentContact,
StudentTeacher,
} from '@/api/infra/demo'
import { onShow } from '@dcloudio/uni-app'
import { nextTick, ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
deleteStudent,
deleteStudentContact,
deleteStudentTeacher,
getStudent,
getStudentContactPage,
getStudentTeacherPage,
} from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
const props = defineProps<{ id?: number }>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<Student>() // 主表数据
const studentContactList = ref<StudentContact[]>([]) // 学生联系人列表
const studentContactTotal = ref(0) // 学生联系人总数
const studentContactPagingRef = ref<any>() // 学生联系人分页组件引用
const studentTeacherList = ref<StudentTeacher[]>([]) // 学生班主任列表
const studentTeacherTotal = ref(0) // 学生班主任总数
const studentTeacherPagingRef = ref<any>() // 学生班主任分页组件引用
const deleting = ref(false) // 主表删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载主表 */
async function loadMain() {
if (props.id) {
formData.value = await getStudent(props.id)
}
}
/** 查询学生联系人列表 */
async function queryStudentContactList(pageNo: number, pageSize: number) {
if (!props.id) {
studentContactPagingRef.value?.completeByTotal([], 0)
return
}
try {
const data = await getStudentContactPage({ studentId: props.id, pageNo, pageSize })
studentContactTotal.value = data.total
studentContactPagingRef.value?.completeByTotal(data.list, data.total)
} catch {
studentContactPagingRef.value?.complete(false)
}
}
/** 打开学生联系人表单 */
function openStudentContact(item?: StudentContact) {
const query = [
`studentId=${props.id}`,
item?.id ? `id=${item.id}` : '',
].filter(Boolean).join('&')
uni.navigateTo({ url: `/pages-infra/demo/student-contact/form/index?${query}` })
}
/** 删除学生联系人 */
async function removeStudentContact(item: StudentContact) {
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生联系人吗?' })
} catch {
return
}
await deleteStudentContact(item.id!)
toast.success('删除成功')
studentContactPagingRef.value?.reload()
}
/** 查询学生班主任列表 */
async function queryStudentTeacherList(pageNo: number, pageSize: number) {
if (!props.id) {
studentTeacherPagingRef.value?.completeByTotal([], 0)
return
}
try {
const data = await getStudentTeacherPage({ studentId: props.id, pageNo, pageSize })
studentTeacherTotal.value = data.total
studentTeacherPagingRef.value?.completeByTotal(data.list, data.total)
} catch {
studentTeacherPagingRef.value?.complete(false)
}
}
/** 打开学生班主任表单 */
function openStudentTeacher(item?: StudentTeacher) {
const query = [
`studentId=${props.id}`,
item?.id ? `id=${item.id}` : '',
].filter(Boolean).join('&')
uni.navigateTo({ url: `/pages-infra/demo/student-teacher/form/index?${query}` })
}
/** 删除学生班主任 */
async function removeStudentTeacher(item: StudentTeacher) {
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生班主任吗?' })
} catch {
return
}
await deleteStudentTeacher(item.id!)
toast.success('删除成功')
studentTeacherPagingRef.value?.reload()
}
/** 编辑主表 */
function handleEdit() {
uni.navigateTo({ url: `/pages-infra/demo/form/index?id=${props.id}` })
}
/** 删除主表 */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生吗?' })
} catch {
return
}
deleting.value = true
try {
await deleteStudent(props.id)
toast.success('删除成功')
uni.$emit('infra:demo:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑和子表返回后刷新) */
onShow(() => {
loadMain()
nextTick(() => {
studentContactPagingRef.value?.reload()
studentTeacherPagingRef.value?.reload()
})
})
</script>
@@ -1,193 +0,0 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input
v-model="formData.name"
clearable
placeholder="请输入名字"
/>
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
show-word-limit
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
<wd-radio-group v-model="formData.enabled" type="button">
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
show-word-limit
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type {
Student,
} from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { createStudent, getStudent, updateStudent } from '@/api/infra/demo'
import { getBoolDictOptions } from '@/hooks/useDict'
import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<Student>>({
id: undefined,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: '',
memo: '',
}) // 表单数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const formSchema = createFormSchema({
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
video: [{ required: true, message: '附件不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载学生详情 */
async function getDetail() {
if (!props.id) {
return
}
formData.value = await getStudent(props.id)
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = formData.value as Student
if (props.id) {
await updateStudent(data)
toast.success('修改成功')
} else {
await createStudent(data)
toast.success('新增成功')
}
uni.$emit('infra:demo:reload')
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>
@@ -1,169 +0,0 @@
<template>
<view class="yd-page-container yd-page-container-paging">
<!-- 顶部导航栏 -->
<wd-navbar
title="学生管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 学生列表 -->
<z-paging
ref="pagingRef"
v-model="list"
:fixed="false"
class="min-h-0 flex-1"
:default-page-size="10"
:refresher-enabled="true"
:inside-more="true"
:loading-more-default-as-loading="true"
empty-view-text="暂无学生数据"
@query="queryList"
>
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.name }}
</view>
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">简介:</text>
<text class="line-clamp-1">{{ item.description }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">出生日期:</text>
<text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">是否有效:</text>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">头像:</text>
<text class="line-clamp-1">{{ item.avatar }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">附件:</text>
<text class="line-clamp-1">{{ item.video }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">备注:</text>
<text class="line-clamp-1">{{ item.memo }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">创建时间:</text>
<text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
</view>
</view>
</view>
</view>
</z-paging>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['infra:student:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
import type { Student } from '@/api/infra/demo'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { getStudentPage } from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const list = ref<Student[]>([]) // 列表数据
const pagingRef = ref<any>() // 分页组件引用
const queryParams = ref<Record<string, any>>({}) // 查询参数
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询学生列表 */
async function queryList(pageNo: number, pageSize: number) {
try {
const params = {
...queryParams.value,
pageNo,
pageSize,
}
const data = await getStudentPage(params)
pagingRef.value?.completeByTotal(data.list, data.total)
} catch {
pagingRef.value?.complete(false)
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = { ...data }
reload()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 重新加载 */
function reload() {
pagingRef.value?.reload()
}
/** 新增学生 */
function handleAdd() {
uni.navigateTo({
url: '/pages-infra/demo/form/index',
})
}
/** 查看详情 */
function handleDetail(item: Student) {
uni.navigateTo({
url: `/pages-infra/demo/detail/index?id=${item.id}`,
})
}
/** 初始化 */
onMounted(() => {
uni.$on('infra:demo:reload', reload)
})
/** 卸载 */
onUnload(() => {
uni.$off('infra:demo:reload', reload)
})
</script>
<style lang="scss" scoped>
</style>
@@ -1,116 +0,0 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
名字
</view>
<wd-input
v-model="formData.name"
placeholder="请输入名字"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
出生日期
</view>
<wd-input
v-model="formData.birthday"
placeholder="请输入出生日期"
clearable
/>
</view>
<yd-search-picker
v-model="formData.sex"
label="性别"
:columns="getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
all-option
/>
<yd-search-picker
v-model="formData.enabled"
label="是否有效"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
all-option
/>
<yd-search-date-range v-model="formData.createTime" label="创建时间" />
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { getNavbarHeight } from '@/utils'
import { formatDate, formatDateRange } from '@/utils/date'
import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false) // 搜索弹窗显示状态
const formData = reactive({
name: undefined as string | undefined,
birthday: undefined as string | undefined,
sex: undefined as number | undefined,
enabled: undefined as boolean | undefined,
createTime: [undefined, undefined] as [number | undefined, number | undefined],
}) // 搜索表单数据
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
if (formData.name) {
conditions.push(`名字:${formData.name}`)
}
if (formData.birthday) {
conditions.push(`出生日期:${formData.birthday}`)
}
if (formData.sex !== undefined) {
conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
}
if (formData.enabled !== undefined) {
conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
}
if (formData.createTime?.[0] && formData.createTime?.[1]) {
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
})
/** 搜索按钮操作 */
function handleSearch() {
visible.value = false
emit('search', {
...formData,
createTime: formatDateRange(formData.createTime),
})
}
/** 重置按钮操作 */
function handleReset() {
formData.name = undefined
formData.birthday = undefined
formData.sex = undefined
formData.enabled = undefined
formData.createTime = [undefined, undefined]
visible.value = false
emit('reset')
}
</script>
@@ -1,167 +0,0 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<!-- 表单区域 -->
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input v-model="formData.name" clearable placeholder="请输入名字" />
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<yd-form-picker
v-model="formData.enabled"
label="是否有效"
label-width="180rpx"
prop="enabled"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
placeholder="请选择是否有效"
/>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type { StudentContact } from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { createStudentContact, getStudentContact, updateStudentContact } from '@/api/infra/demo'
import { delay, navigateBackPlus } from '@/utils'
import { getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
studentId: number
}>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生联系人' : '新增学生联系人')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<StudentContact>>({
id: undefined,
studentId: props.studentId,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: undefined,
memo: '',
}) // 表单数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const formSchema = createFormSchema({
studentId: [{ required: true, message: '学生编号不能为空' }],
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 加载学生联系人详情 */
async function getDetail() {
if (props.id) {
formData.value = await getStudentContact(props.id)
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
studentId: props.studentId ?? formData.value.studentId,
} as StudentContact
if (props.id) {
await updateStudentContact(data)
toast.success('修改成功')
} else {
await createStudentContact(data)
toast.success('新增成功')
}
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
@@ -1,167 +0,0 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<!-- 表单区域 -->
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input v-model="formData.name" clearable placeholder="请输入名字" />
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<yd-form-picker
v-model="formData.enabled"
label="是否有效"
label-width="180rpx"
prop="enabled"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
placeholder="请选择是否有效"
/>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type { StudentTeacher } from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { createStudentTeacher, getStudentTeacher, updateStudentTeacher } from '@/api/infra/demo'
import { delay, navigateBackPlus } from '@/utils'
import { getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
studentId: number
}>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生班主任' : '新增学生班主任')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<StudentTeacher>>({
id: undefined,
studentId: props.studentId,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: undefined,
memo: '',
}) // 表单数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const formSchema = createFormSchema({
studentId: [{ required: true, message: '学生编号不能为空' }],
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 加载学生班主任详情 */
async function getDetail() {
if (props.id) {
formData.value = await getStudentTeacher(props.id)
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
studentId: props.studentId ?? formData.value.studentId,
} as StudentTeacher
if (props.id) {
await updateStudentTeacher(data)
toast.success('修改成功')
} else {
await createStudentTeacher(data)
toast.success('新增成功')
}
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>
@@ -1,67 +0,0 @@
[ {
"contentPath" : "java/InfraStudentPageReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
}, {
"contentPath" : "java/InfraStudentRespVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
}, {
"contentPath" : "java/InfraStudentSaveReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
}, {
"contentPath" : "java/InfraStudentController",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
}, {
"contentPath" : "java/InfraStudentDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
}, {
"contentPath" : "java/InfraStudentContactDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
}, {
"contentPath" : "java/InfraStudentTeacherDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
}, {
"contentPath" : "java/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
}, {
"contentPath" : "java/InfraStudentContactMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
}, {
"contentPath" : "java/InfraStudentTeacherMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
}, {
"contentPath" : "xml/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
}, {
"contentPath" : "java/InfraStudentServiceImpl",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
}, {
"contentPath" : "java/InfraStudentService",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
}, {
"contentPath" : "java/InfraStudentServiceImplTest",
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
}, {
"contentPath" : "java/ErrorCodeConstants_手动操作",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
}, {
"contentPath" : "sql/sql",
"filePath" : "sql/sql.sql"
}, {
"contentPath" : "sql/h2",
"filePath" : "sql/h2.sql"
}, {
"contentPath" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
} ]
@@ -1,3 +0,0 @@
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
// ========== 学生 TODO 补充编号 ==========
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
@@ -1,71 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生联系人 DO
*
* @author 芋道源码
*/
@TableName("infra_student_contact")
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentContactDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 学生编号
*/
private Long studentId;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 学生联系人 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
return selectList(InfraStudentContactDO::getStudentId, studentId);
}
default int deleteByStudentId(Long studentId) {
return delete(InfraStudentContactDO::getStudentId, studentId);
}
default int deleteByStudentIds(List<Long> studentIds) {
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
}
}
@@ -1,126 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.*;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
@Tag(name = "管理后台 - 学生")
@RestController
@RequestMapping("/infra/student")
@Validated
public class InfraStudentController {
@Resource
private InfraStudentService studentService;
@PostMapping("/create")
@Operation(summary = "创建学生")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
return success(studentService.createStudent(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新学生")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
studentService.updateStudent(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除学生")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
studentService.deleteStudent(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得学生")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
InfraStudentDO student = studentService.getStudent(id);
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得学生分页")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出学生 Excel")
@PreAuthorize("@ss.hasPermission('infra:student:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
BeanUtils.toBean(list, InfraStudentRespVO.class));
}
// ==================== 子表(学生联系人) ====================
@GetMapping("/student-contact/list-by-student-id")
@Operation(summary = "获得学生联系人列表")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentContactListByStudentId(studentId));
}
// ==================== 子表(学生班主任) ====================
@GetMapping("/student-teacher/get-by-student-id")
@Operation(summary = "获得学生班主任")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentTeacherByStudentId(studentId));
}
}
@@ -1,68 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生 DO
*
* @author 芋道源码
*/
@TableName("infra_student")
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,30 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
/**
* 学生 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(InfraStudentDO::getId));
}
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 学生分页 Request VO")
@Data
public class InfraStudentPageReqVO extends PageParam {
@Schema(description = "名字", example = "芋头")
private String name;
@Schema(description = "出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", example = "1")
private Integer sex;
@Schema(description = "是否有效", example = "true")
private Boolean enabled;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -1,59 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.idev.excel.annotation.*;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
@Schema(description = "管理后台 - 学生 Response VO")
@Data
@ExcelIgnoreUnannotated
public class InfraStudentRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("编号")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@ExcelProperty("名字")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@ExcelProperty("简介")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "性别", converter = DictConvert.class)
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@ExcelProperty("头像")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@ExcelProperty("附件")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@ExcelProperty("备注")
private String memo;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}
@@ -1,57 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
@Data
public class InfraStudentSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@NotEmpty(message = "名字不能为空")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@NotEmpty(message = "简介不能为空")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "出生日期不能为空")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "性别不能为空")
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否有效不能为空")
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@NotEmpty(message = "头像不能为空")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@NotEmpty(message = "附件不能为空")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@NotEmpty(message = "备注不能为空")
private String memo;
@Schema(description = "学生联系人列表")
private List<InfraStudentContactDO> studentContacts;
@Schema(description = "学生班主任")
private InfraStudentTeacherDO studentTeacher;
}
@@ -1,84 +0,0 @@
package cn.iocoder.yudao.module.infra.service.demo;
import java.util.*;
import jakarta.validation.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* 学生 Service 接口
*
* @author 芋道源码
*/
public interface InfraStudentService {
/**
* 创建学生
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
/**
* 更新学生
*
* @param updateReqVO 更新信息
*/
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
/**
* 删除学生
*
* @param id 编号
*/
void deleteStudent(Long id);
/**
* 批量删除学生
*
* @param ids 编号
*/
void deleteStudentListByIds(List<Long> ids);
/**
* 获得学生
*
* @param id 编号
* @return 学生
*/
InfraStudentDO getStudent(Long id);
/**
* 获得学生分页
*
* @param pageReqVO 分页查询
* @return 学生分页
*/
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
// ==================== 子表(学生联系人) ====================
/**
* 获得学生联系人列表
*
* @param studentId 学生编号
* @return 学生联系人列表
*/
List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
// ==================== 子表(学生班主任) ====================
/**
* 获得学生班主任
*
* @param studentId 学生编号
* @return 学生班主任
*/
InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
}
@@ -1,191 +0,0 @@
package cn.iocoder.yudao.module.infra.service.demo;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
/**
* 学生 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class InfraStudentServiceImpl implements InfraStudentService {
@Resource
private InfraStudentMapper studentMapper;
@Resource
private InfraStudentContactMapper studentContactMapper;
@Resource
private InfraStudentTeacherMapper studentTeacherMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
// 插入
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
studentMapper.insert(student);
// 插入子表
createStudentContactList(student.getId(), createReqVO.getStudentContacts());
createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
// 返回
return student.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
// 校验存在
validateStudentExists(updateReqVO.getId());
// 更新
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
studentMapper.updateById(updateObj);
// 更新子表
updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStudent(Long id) {
// 校验存在
validateStudentExists(id);
// 删除
studentMapper.deleteById(id);
// 删除子表
deleteStudentContactByStudentId(id);
deleteStudentTeacherByStudentId(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStudentListByIds(List<Long> ids) {
// 删除
studentMapper.deleteByIds(ids);
// 删除子表
deleteStudentContactByStudentIds(ids);
deleteStudentTeacherByStudentIds(ids);
}
private void validateStudentExists(Long id) {
if (studentMapper.selectById(id) == null) {
throw exception(STUDENT_NOT_EXISTS);
}
}
@Override
public InfraStudentDO getStudent(Long id) {
return studentMapper.selectById(id);
}
@Override
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
return studentMapper.selectPage(pageReqVO);
}
// ==================== 子表(学生联系人) ====================
@Override
public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
return studentContactMapper.selectListByStudentId(studentId);
}
private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
if (CollUtil.isEmpty(list)) {
return;
}
list.forEach(o -> o.setStudentId(studentId).clean());
studentContactMapper.insertBatch(list);
}
private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
list.forEach(o -> o.setStudentId(studentId).clean());
List<InfraStudentContactDO> oldList = studentContactMapper.selectListByStudentId(studentId);
List<List<InfraStudentContactDO>> diffList = diffList(oldList, list, (oldVal, newVal) -> {
boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
if (same) {
newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
}
return same;
});
// 第二步,批量添加、修改、删除
if (CollUtil.isNotEmpty(diffList.get(0))) {
studentContactMapper.insertBatch(diffList.get(0));
}
if (CollUtil.isNotEmpty(diffList.get(1))) {
studentContactMapper.updateBatch(diffList.get(1));
}
if (CollUtil.isNotEmpty(diffList.get(2))) {
studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
}
}
private void deleteStudentContactByStudentId(Long studentId) {
studentContactMapper.deleteByStudentId(studentId);
}
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
studentContactMapper.deleteByStudentIds(studentIds);
}
// ==================== 子表(学生班主任) ====================
@Override
public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
return studentTeacherMapper.selectByStudentId(studentId);
}
private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
if (studentTeacher == null) {
return;
}
studentTeacher.setStudentId(studentId);
studentTeacherMapper.insert(studentTeacher);
}
private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
if (studentTeacher == null) {
return;
}
studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
studentTeacherMapper.insertOrUpdate(studentTeacher);
}
private void deleteStudentTeacherByStudentId(Long studentId) {
studentTeacherMapper.deleteByStudentId(studentId);
}
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
studentTeacherMapper.deleteByStudentIds(studentIds);
}
}
@@ -1,145 +0,0 @@
package cn.iocoder.yudao.module.infra.service.demo;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Import;
import java.util.*;
import java.time.LocalDateTime;
import static cn.hutool.core.util.RandomUtil.*;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link InfraStudentServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(InfraStudentServiceImpl.class)
public class InfraStudentServiceImplTest extends BaseDbUnitTest {
@Resource
private InfraStudentServiceImpl studentService;
@Resource
private InfraStudentMapper studentMapper;
@Test
public void testCreateStudent_success() {
// 准备参数
InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
// 调用
Long studentId = studentService.createStudent(createReqVO);
// 断言
assertNotNull(studentId);
// 校验记录的属性是否正确
InfraStudentDO student = studentMapper.selectById(studentId);
assertPojoEquals(createReqVO, student, "id");
}
@Test
public void testUpdateStudent_success() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
// 准备参数
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
o.setId(dbStudent.getId()); // 设置更新的 ID
});
// 调用
studentService.updateStudent(updateReqVO);
// 校验是否更新正确
InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
assertPojoEquals(updateReqVO, student);
}
@Test
public void testUpdateStudent_notExists() {
// 准备参数
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
}
@Test
public void testDeleteStudent_success() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbStudent.getId();
// 调用
studentService.deleteStudent(id);
// 校验数据不存在了
assertNull(studentMapper.selectById(id));
}
@Test
public void testDeleteStudent_notExists() {
// 准备参数
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
}
@Test
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
public void testGetStudentPage() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
o.setName(null);
o.setBirthday(null);
o.setSex(null);
o.setEnabled(null);
o.setCreateTime(null);
});
studentMapper.insert(dbStudent);
// 测试 name 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
// 测试 birthday 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
// 测试 sex 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
// 测试 enabled 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
// 测试 createTime 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
// 准备参数
InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
reqVO.setName(null);
reqVO.setBirthday(null);
reqVO.setSex(null);
reqVO.setEnabled(null);
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
// 调用
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbStudent, pageResult.getList().get(0));
}
}
@@ -1,71 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生班主任 DO
*
* @author 芋道源码
*/
@TableName("infra_student_teacher")
@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentTeacherDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 学生编号
*/
private Long studentId;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 学生班主任 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
}
default int deleteByStudentId(Long studentId) {
return delete(InfraStudentTeacherDO::getStudentId, studentId);
}
default int deleteByStudentIds(List<Long> studentIds) {
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
}
}
@@ -1,17 +0,0 @@
-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
CREATE TABLE IF NOT EXISTS "infra_student" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar NOT NULL,
"description" varchar NOT NULL,
"birthday" varchar NOT NULL,
"sex" int NOT NULL,
"enabled" bit NOT NULL,
"avatar" varchar NOT NULL,
"video" varchar NOT NULL,
"memo" varchar NOT NULL,
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("id")
) COMMENT '学生表';
-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
DELETE FROM "infra_student";
@@ -1,54 +0,0 @@
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'学生管理', '', 2, 0, 888,
'student', '', 'infra/demo/index', 0, 'InfraStudent'
);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生查询', 'infra:student:query', 3, 1, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生创建', 'infra:student:create', 3, 2, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生更新', 'infra:student:update', 3, 3, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生删除', 'infra:student:delete', 3, 4, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生导出', 'infra:student:export', 3, 5, @parentId,
'', '', '', 0
);
@@ -1,87 +0,0 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
/** 学生联系人信息 */
export interface StudentContact {
id?: number
studentId?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生班主任信息 */
export interface StudentTeacher {
id?: number
studentId?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生信息 */
export interface Student {
id?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video: string
memo: string
createTime?: number
studentContacts?: StudentContact[]
studentTeacher?: StudentTeacher | null
}
/** 获取学生分页列表 */
export function getStudentPage(params: PageParam) {
return http.get<PageResult<Student>>('/infra/student/page', params)
}
/** 获取学生详情 */
export function getStudent(id: number) {
return http.get<Student>(`/infra/student/get?id=${id}`)
}
/** 创建学生 */
export function createStudent(data: Student) {
return http.post<number>('/infra/student/create', data)
}
/** 更新学生 */
export function updateStudent(data: Student) {
return http.put<boolean>('/infra/student/update', data)
}
/** 删除学生 */
export function deleteStudent(id: number) {
return http.delete<boolean>(`/infra/student/delete?id=${id}`)
}
// ==================== 子表:学生联系人 ====================
/** 获取学生联系人列表 */
export function getStudentContactListByStudentId(studentId: number) {
return http.get<StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`)
}
// ==================== 子表:学生班主任 ====================
/** 获取学生班主任 */
export function getStudentTeacherByStudentId(studentId: number) {
return http.get<StudentTeacher | null>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`)
}
@@ -1,229 +0,0 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="学生详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="学生信息">
<wd-cell title="编号" :value="formData?.id ?? '-'" />
<wd-cell title="名字" :value="formData?.name ?? '-'" />
<wd-cell title="简介" :value="formData?.description ?? '-'" />
<wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
<wd-cell title="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
</wd-cell>
<wd-cell title="是否有效">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
</wd-cell>
<wd-cell title="头像" :value="formData?.avatar ?? '-'" />
<wd-cell title="附件" :value="formData?.video ?? '-'" />
<wd-cell title="备注" :value="formData?.memo ?? '-'" />
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
</wd-cell-group>
<!-- 子表:学生联系人 -->
<view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
学生联系人({{ studentContacts.length }})
</view>
<view class="px-24rpx">
<view v-if="!studentContacts.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无学生联系人
</view>
<view
v-for="item in studentContacts"
:key="item.id"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-8rpx text-28rpx text-[#666]">
学生编号:{{ item.studentId ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
名字:{{ item.name ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
简介:{{ item.description ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
出生日期:{{ item.birthday ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
性别:{{ item.sex ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
是否有效:{{ item.enabled ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
头像:{{ item.avatar ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
附件:{{ item.video ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
备注:{{ item.memo ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
创建时间:{{ item.createTime ?? '-' }}
</view>
</view>
</view>
<!-- 子表:学生班主任 -->
<view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
学生班主任
</view>
<view class="px-24rpx">
<view class="rounded-12rpx bg-white p-24rpx shadow-sm">
<template v-if="studentTeacher">
<view class="mb-8rpx text-28rpx text-[#666]">
学生编号:{{ studentTeacher.studentId ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
名字:{{ studentTeacher.name ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
简介:{{ studentTeacher.description ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
出生日期:{{ studentTeacher.birthday ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
性别:{{ studentTeacher.sex ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
是否有效:{{ studentTeacher.enabled ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
头像:{{ studentTeacher.avatar ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
附件:{{ studentTeacher.video ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
备注:{{ studentTeacher.memo ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
创建时间:{{ studentTeacher.createTime ?? '-' }}
</view>
</template>
<view v-else class="text-center text-26rpx text-[#999]">
暂无学生班主任
</view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button
v-if="hasAccessByCodes(['infra:student:update'])"
class="flex-1" type="warning" @click="handleEdit"
>
编辑
</wd-button>
<wd-button
v-if="hasAccessByCodes(['infra:student:delete'])"
class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import type {
Student,
StudentContact,
StudentTeacher,
} from '@/api/infra/demo'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
deleteStudent,
getStudent,
getStudentContactListByStudentId,
getStudentTeacherByStudentId,
} from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
const props = defineProps<{
id?: number
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<Student>() // 主表数据
const studentContacts = ref<StudentContact[]>([]) // 学生联系人数据
const studentTeacher = ref<StudentTeacher | null>(null) // 学生班主任数据
const deleting = ref(false) // 删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载主表和子表详情 */
async function getDetail() {
if (!props.id || deleting.value) {
return
}
const [data, studentContactData, studentTeacherData] = await Promise.all([
getStudent(props.id),
getStudentContactListByStudentId(props.id),
getStudentTeacherByStudentId(props.id),
])
formData.value = data
studentContacts.value = studentContactData || []
studentTeacher.value = studentTeacherData || null
}
/** 编辑学生 */
function handleEdit() {
uni.navigateTo({ url: `/pages-infra/demo/form/index?id=${props.id}` })
}
/** 删除学生 */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生吗?' })
} catch {
return
}
deleting.value = true
try {
await deleteStudent(props.id)
toast.success('删除成功')
uni.$emit('infra:demo:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑返回后刷新) */
onShow(() => {
getDetail()
})
</script>
@@ -1,480 +0,0 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input
v-model="formData.name"
clearable
placeholder="请输入名字"
/>
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
show-word-limit
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
<wd-radio-group v-model="formData.enabled" type="button">
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
show-word-limit
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
<!-- 子表:学生联系人 -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">学生联系人</text>
<wd-button size="small" type="primary" variant="plain" @click="addStudentContact">
添加
</wd-button>
</view>
<view class="px-24rpx">
<view v-if="!studentContacts.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无学生联系人
</view>
<view
v-for="(item, index) in studentContacts"
:key="index"
class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-16rpx flex justify-end">
<wd-button size="small" type="danger" variant="plain" @click="removeStudentContact(index)">
删除
</wd-button>
</view>
<wd-form-item title="名字" title-width="180rpx">
<wd-input v-model="item.name" clearable placeholder="请输入名字" />
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx">
<wd-textarea
v-model="item.description"
clearable
placeholder="请输入简介"
:maxlength="200"
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
is-link
:value="item.birthday ? formatDateTime(item.birthday) : ''"
placeholder="请选择出生日期"
@click="subDateVisible[`studentContacts.${index}.birthday`] = true"
/>
<wd-datetime-picker
v-model:visible="subDateVisible[`studentContacts.${index}.birthday`]"
:model-value="item.birthday || Date.now()"
type="datetime"
@update:model-value="value => item.birthday = value"
/>
<yd-form-picker
v-model="item.sex"
label="性别"
label-width="180rpx"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<wd-form-item title="是否有效" title-width="180rpx" center>
<wd-radio-group v-model="item.enabled" type="button">
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item title="头像" title-width="180rpx">
<yd-upload-img v-model="item.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx">
<yd-upload-file v-model="item.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx">
<wd-textarea
v-model="item.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
/>
</wd-form-item>
</view>
</view>
<!-- 子表:学生班主任 -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">学生班主任</text>
<wd-button
v-if="!studentTeacher"
size="small"
type="primary"
variant="plain"
@click="addStudentTeacher()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<view v-if="!studentTeacher" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无学生班主任
</view>
<view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
<wd-form-item title="名字" title-width="180rpx">
<wd-input v-model="studentTeacher.name" clearable placeholder="请输入名字" />
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx">
<wd-textarea
v-model="studentTeacher.description"
clearable
placeholder="请输入简介"
:maxlength="200"
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
is-link
:value="studentTeacher.birthday ? formatDateTime(studentTeacher.birthday) : ''"
placeholder="请选择出生日期"
@click="subDateVisible['studentTeacher.birthday'] = true"
/>
<wd-datetime-picker
v-model:visible="subDateVisible['studentTeacher.birthday']"
:model-value="studentTeacher.birthday || Date.now()"
type="datetime"
@update:model-value="value => studentTeacher.birthday = value"
/>
<yd-form-picker
v-model="studentTeacher.sex"
label="性别"
label-width="180rpx"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<wd-form-item title="是否有效" title-width="180rpx" center>
<wd-radio-group v-model="studentTeacher.enabled" type="button">
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item title="头像" title-width="180rpx">
<yd-upload-img v-model="studentTeacher.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx">
<yd-upload-file v-model="studentTeacher.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx">
<wd-textarea
v-model="studentTeacher.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
/>
</wd-form-item>
</view>
</view>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type {
Student,
StudentContact,
StudentTeacher,
} from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
createStudent,
getStudent,
getStudentContactListByStudentId,
getStudentTeacherByStudentId,
updateStudent,
} from '@/api/infra/demo'
import { getBoolDictOptions } from '@/hooks/useDict'
import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<Student>>({
id: undefined,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: '',
memo: '',
}) // 表单数据
const studentContacts = ref<Partial<StudentContact>[]>([]) // 学生联系人数据
const studentTeacher = ref<Partial<StudentTeacher> | null>(null) // 学生班主任数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const subDateVisible = ref<Record<string, boolean>>({}) // 子表日期选择器状态
const formSchema = createFormSchema({
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
video: [{ required: true, message: '附件不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 添加学生联系人 */
function addStudentContact() {
studentContacts.value.push({
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: undefined,
memo: '',
})
}
/** 删除学生联系人 */
function removeStudentContact(index: number) {
studentContacts.value.splice(index, 1)
}
/** 添加学生班主任 */
function addStudentTeacher() {
studentTeacher.value = {
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: undefined,
memo: '',
}
}
/** 校验主子表数据 */
function isEmptySubValue(value: unknown) {
return value === undefined || value === null || value === ''
}
function validateSubTables() {
for (let index = 0; index < studentContacts.value.length; index++) {
const item = studentContacts.value[index]!
if (isEmptySubValue(item.name)) {
toast.warning(`学生联系人第${index + 1}项的名字不能为空`)
return false
}
if (isEmptySubValue(item.description)) {
toast.warning(`学生联系人第${index + 1}项的简介不能为空`)
return false
}
if (isEmptySubValue(item.birthday)) {
toast.warning(`学生联系人第${index + 1}项的出生日期不能为空`)
return false
}
if (isEmptySubValue(item.sex)) {
toast.warning(`学生联系人第${index + 1}项的性别不能为空`)
return false
}
if (isEmptySubValue(item.enabled)) {
toast.warning(`学生联系人第${index + 1}项的是否有效不能为空`)
return false
}
if (isEmptySubValue(item.avatar)) {
toast.warning(`学生联系人第${index + 1}项的头像不能为空`)
return false
}
if (isEmptySubValue(item.memo)) {
toast.warning(`学生联系人第${index + 1}项的备注不能为空`)
return false
}
}
if (studentTeacher.value) {
if (isEmptySubValue(studentTeacher.value.name)) {
toast.warning('学生班主任的名字不能为空')
return false
}
if (isEmptySubValue(studentTeacher.value.description)) {
toast.warning('学生班主任的简介不能为空')
return false
}
if (isEmptySubValue(studentTeacher.value.birthday)) {
toast.warning('学生班主任的出生日期不能为空')
return false
}
if (isEmptySubValue(studentTeacher.value.sex)) {
toast.warning('学生班主任的性别不能为空')
return false
}
if (isEmptySubValue(studentTeacher.value.enabled)) {
toast.warning('学生班主任的是否有效不能为空')
return false
}
if (isEmptySubValue(studentTeacher.value.avatar)) {
toast.warning('学生班主任的头像不能为空')
return false
}
if (isEmptySubValue(studentTeacher.value.memo)) {
toast.warning('学生班主任的备注不能为空')
return false
}
}
return true
}
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载学生详情 */
async function getDetail() {
if (!props.id) {
return
}
const [data, studentContactData, studentTeacherData] = await Promise.all([
getStudent(props.id),
getStudentContactListByStudentId(props.id),
getStudentTeacherByStudentId(props.id),
])
formData.value = data
studentContacts.value = studentContactData || []
studentTeacher.value = studentTeacherData || null
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
if (!validateSubTables()) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
studentContacts: studentContacts.value,
studentTeacher: studentTeacher.value,
} as Student
if (props.id) {
await updateStudent(data)
toast.success('修改成功')
} else {
await createStudent(data)
toast.success('新增成功')
}
uni.$emit('infra:demo:reload')
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>
@@ -1,169 +0,0 @@
<template>
<view class="yd-page-container yd-page-container-paging">
<!-- 顶部导航栏 -->
<wd-navbar
title="学生管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 学生列表 -->
<z-paging
ref="pagingRef"
v-model="list"
:fixed="false"
class="min-h-0 flex-1"
:default-page-size="10"
:refresher-enabled="true"
:inside-more="true"
:loading-more-default-as-loading="true"
empty-view-text="暂无学生数据"
@query="queryList"
>
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.name }}
</view>
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">简介:</text>
<text class="line-clamp-1">{{ item.description }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">出生日期:</text>
<text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">是否有效:</text>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">头像:</text>
<text class="line-clamp-1">{{ item.avatar }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">附件:</text>
<text class="line-clamp-1">{{ item.video }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">备注:</text>
<text class="line-clamp-1">{{ item.memo }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">创建时间:</text>
<text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
</view>
</view>
</view>
</view>
</z-paging>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['infra:student:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
import type { Student } from '@/api/infra/demo'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { getStudentPage } from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const list = ref<Student[]>([]) // 列表数据
const pagingRef = ref<any>() // 分页组件引用
const queryParams = ref<Record<string, any>>({}) // 查询参数
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询学生列表 */
async function queryList(pageNo: number, pageSize: number) {
try {
const params = {
...queryParams.value,
pageNo,
pageSize,
}
const data = await getStudentPage(params)
pagingRef.value?.completeByTotal(data.list, data.total)
} catch {
pagingRef.value?.complete(false)
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = { ...data }
reload()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 重新加载 */
function reload() {
pagingRef.value?.reload()
}
/** 新增学生 */
function handleAdd() {
uni.navigateTo({
url: '/pages-infra/demo/form/index',
})
}
/** 查看详情 */
function handleDetail(item: Student) {
uni.navigateTo({
url: `/pages-infra/demo/detail/index?id=${item.id}`,
})
}
/** 初始化 */
onMounted(() => {
uni.$on('infra:demo:reload', reload)
})
/** 卸载 */
onUnload(() => {
uni.$off('infra:demo:reload', reload)
})
</script>
<style lang="scss" scoped>
</style>
@@ -1,116 +0,0 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
名字
</view>
<wd-input
v-model="formData.name"
placeholder="请输入名字"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
出生日期
</view>
<wd-input
v-model="formData.birthday"
placeholder="请输入出生日期"
clearable
/>
</view>
<yd-search-picker
v-model="formData.sex"
label="性别"
:columns="getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
all-option
/>
<yd-search-picker
v-model="formData.enabled"
label="是否有效"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
all-option
/>
<yd-search-date-range v-model="formData.createTime" label="创建时间" />
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { getNavbarHeight } from '@/utils'
import { formatDate, formatDateRange } from '@/utils/date'
import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false) // 搜索弹窗显示状态
const formData = reactive({
name: undefined as string | undefined,
birthday: undefined as string | undefined,
sex: undefined as number | undefined,
enabled: undefined as boolean | undefined,
createTime: [undefined, undefined] as [number | undefined, number | undefined],
}) // 搜索表单数据
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
if (formData.name) {
conditions.push(`名字:${formData.name}`)
}
if (formData.birthday) {
conditions.push(`出生日期:${formData.birthday}`)
}
if (formData.sex !== undefined) {
conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
}
if (formData.enabled !== undefined) {
conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
}
if (formData.createTime?.[0] && formData.createTime?.[1]) {
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
})
/** 搜索按钮操作 */
function handleSearch() {
visible.value = false
emit('search', {
...formData,
createTime: formatDateRange(formData.createTime),
})
}
/** 重置按钮操作 */
function handleReset() {
formData.name = undefined
formData.birthday = undefined
formData.sex = undefined
formData.enabled = undefined
formData.createTime = [undefined, undefined]
visible.value = false
emit('reset')
}
</script>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>
@@ -1,67 +0,0 @@
[ {
"contentPath" : "java/InfraStudentPageReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
}, {
"contentPath" : "java/InfraStudentRespVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
}, {
"contentPath" : "java/InfraStudentSaveReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
}, {
"contentPath" : "java/InfraStudentController",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
}, {
"contentPath" : "java/InfraStudentDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
}, {
"contentPath" : "java/InfraStudentContactDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
}, {
"contentPath" : "java/InfraStudentTeacherDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
}, {
"contentPath" : "java/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
}, {
"contentPath" : "java/InfraStudentContactMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
}, {
"contentPath" : "java/InfraStudentTeacherMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
}, {
"contentPath" : "xml/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
}, {
"contentPath" : "java/InfraStudentServiceImpl",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
}, {
"contentPath" : "java/InfraStudentService",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
}, {
"contentPath" : "java/InfraStudentServiceImplTest",
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
}, {
"contentPath" : "java/ErrorCodeConstants_手动操作",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
}, {
"contentPath" : "sql/sql",
"filePath" : "sql/sql.sql"
}, {
"contentPath" : "sql/h2",
"filePath" : "sql/h2.sql"
}, {
"contentPath" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
} ]
@@ -1,3 +0,0 @@
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
// ========== 学生 TODO 补充编号 ==========
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
@@ -1,71 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生联系人 DO
*
* @author 芋道源码
*/
@TableName("infra_student_contact")
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentContactDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 学生编号
*/
private Long studentId;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 学生联系人 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
return selectList(InfraStudentContactDO::getStudentId, studentId);
}
default int deleteByStudentId(Long studentId) {
return delete(InfraStudentContactDO::getStudentId, studentId);
}
default int deleteByStudentIds(List<Long> studentIds) {
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
}
}
@@ -1,126 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.*;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
@Tag(name = "管理后台 - 学生")
@RestController
@RequestMapping("/infra/student")
@Validated
public class InfraStudentController {
@Resource
private InfraStudentService studentService;
@PostMapping("/create")
@Operation(summary = "创建学生")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
return success(studentService.createStudent(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新学生")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
studentService.updateStudent(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除学生")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
studentService.deleteStudent(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得学生")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
InfraStudentDO student = studentService.getStudent(id);
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得学生分页")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出学生 Excel")
@PreAuthorize("@ss.hasPermission('infra:student:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
BeanUtils.toBean(list, InfraStudentRespVO.class));
}
// ==================== 子表(学生联系人) ====================
@GetMapping("/student-contact/list-by-student-id")
@Operation(summary = "获得学生联系人列表")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentContactListByStudentId(studentId));
}
// ==================== 子表(学生班主任) ====================
@GetMapping("/student-teacher/get-by-student-id")
@Operation(summary = "获得学生班主任")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentTeacherByStudentId(studentId));
}
}
@@ -1,68 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生 DO
*
* @author 芋道源码
*/
@TableName("infra_student")
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -1,30 +0,0 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
/**
* 学生 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(InfraStudentDO::getId));
}
}
@@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 学生分页 Request VO")
@Data
public class InfraStudentPageReqVO extends PageParam {
@Schema(description = "名字", example = "芋头")
private String name;
@Schema(description = "出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", example = "1")
private Integer sex;
@Schema(description = "是否有效", example = "true")
private Boolean enabled;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -1,59 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.idev.excel.annotation.*;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
@Schema(description = "管理后台 - 学生 Response VO")
@Data
@ExcelIgnoreUnannotated
public class InfraStudentRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("编号")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@ExcelProperty("名字")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@ExcelProperty("简介")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "性别", converter = DictConvert.class)
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@ExcelProperty("头像")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@ExcelProperty("附件")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@ExcelProperty("备注")
private String memo;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}
@@ -1,57 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
@Data
public class InfraStudentSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@NotEmpty(message = "名字不能为空")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@NotEmpty(message = "简介不能为空")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "出生日期不能为空")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "性别不能为空")
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否有效不能为空")
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@NotEmpty(message = "头像不能为空")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@NotEmpty(message = "附件不能为空")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@NotEmpty(message = "备注不能为空")
private String memo;
@Schema(description = "学生联系人列表")
private List<InfraStudentContactDO> studentContacts;
@Schema(description = "学生班主任")
private InfraStudentTeacherDO studentTeacher;
}

Some files were not shown because too many files have changed in this diff Show More