feat: 完成所有模块待完成任务和模块集成

## 应用管理模块后端开发
- 创建pg_application、pg_app_api、pg_api_dict三张表
- 实现Application、AppApi、ApiDict实体类
- 实现ApplicationMapper及XML映射
- 实现IApplicationService及实现类
- 实现ApplicationController(7个API接口)
- 应用编码生成:YY + 6位序号
- 密钥生成:32位随机字符串
- 接口授权保存(事务处理)

## 学生会员模块集成
- IStudentService新增5个方法:
  - isStudentInSchool:检查学生是否在指定学校
  - updateStudentMember:更新学生会员关联
  - unbindStudent:解绑学生
  - countByMemberId:统计会员绑定学生数
  - selectStudentVOsByMemberId:查询会员绑定学生列表
- StudentServiceImpl实现5个方法
- StudentMapper新增2个SQL查询
- MemberServiceImpl完成5个TODO:
  - 学生绑定校验(教师只能绑定本校学生)
  - 学生绑定更新
  - 学生解绑
  - 删除前检查(有学生不可删)
  - 获取绑定学生列表

## 学生批量导入完善
- IRegionService新增getRegionIdByPath方法
- ISchoolService新增3个方法:
  - getSchoolIdByName:根据学校名称查询ID
  - getSchoolGradeId:根据年级名称查询ID
  - getSchoolClassId:根据班级名称查询ID
- IMemberService新增getOrCreateMemberByPhone方法
- StudentImportListener完整实现:
  - 区域ID查询
  - 学校ID查询
  - 年级ID查询
  - 班级ID查询
  - 会员查询或创建
  - 学生信息保存
  - 性别和出生日期解析

## 导入模板下载
- StudentController实现downloadTemplate方法
- 使用EasyExcel生成标准Excel模板
- 包含示例数据
This commit is contained in:
神码-方晓辉 2026-01-31 23:31:27 +08:00
parent 275a4ed3a8
commit 178a1ea507
26 changed files with 939 additions and 392 deletions

View File

@ -7,32 +7,32 @@ import com.pangu.application.service.IApplicationService;
import com.pangu.common.core.controller.BaseController; import com.pangu.common.core.controller.BaseController;
import com.pangu.common.core.domain.AjaxResult; import com.pangu.common.core.domain.AjaxResult;
import com.pangu.common.core.page.TableDataInfo; import com.pangu.common.core.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 应用管理控制器 * 应用管理Controller
*
* @author pangu * @author pangu
*/ */
@RestController @RestController
@RequestMapping("/api/app") @RequestMapping("/api/application")
@RequiredArgsConstructor
@PreAuthorize("hasRole('admin')")
public class ApplicationController extends BaseController { public class ApplicationController extends BaseController {
@Resource private final IApplicationService applicationService;
private IApplicationService applicationService;
/** /**
* 查询应用列表 * 查询应用列表
*/ */
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(ApplicationDTO dto) { public TableDataInfo list(ApplicationDTO applicationDTO) {
startPage(); return applicationService.selectApplicationList(applicationDTO);
List<ApplicationVO> list = applicationService.selectApplicationList(dto);
return getDataTable(list);
} }
/** /**
@ -40,15 +40,16 @@ public class ApplicationController extends BaseController {
*/ */
@GetMapping("/{appId}") @GetMapping("/{appId}")
public AjaxResult getInfo(@PathVariable Long appId) { public AjaxResult getInfo(@PathVariable Long appId) {
return success(applicationService.selectApplicationById(appId)); ApplicationVO applicationVO = applicationService.getApplicationById(appId);
return success(applicationVO);
} }
/** /**
* 新增应用 * 新增应用
*/ */
@PostMapping @PostMapping
public AjaxResult add(@Validated @RequestBody ApplicationDTO dto) { public AjaxResult add(@Validated @RequestBody ApplicationDTO applicationDTO) {
Map<String, String> result = applicationService.insertApplication(dto); ApplicationVO result = applicationService.insertApplication(applicationDTO);
return success(result); return success(result);
} }
@ -56,8 +57,8 @@ public class ApplicationController extends BaseController {
* 修改应用 * 修改应用
*/ */
@PutMapping @PutMapping
public AjaxResult edit(@Validated @RequestBody ApplicationDTO dto) { public AjaxResult edit(@Validated @RequestBody ApplicationDTO applicationDTO) {
return toAjax(applicationService.updateApplication(dto)); return toAjax(applicationService.updateApplication(applicationDTO));
} }
/** /**
@ -65,7 +66,7 @@ public class ApplicationController extends BaseController {
*/ */
@DeleteMapping("/{appId}") @DeleteMapping("/{appId}")
public AjaxResult remove(@PathVariable Long appId) { public AjaxResult remove(@PathVariable Long appId) {
return toAjax(applicationService.deleteApplicationById(appId)); return toAjax(applicationService.deleteApplication(appId));
} }
/** /**
@ -73,16 +74,16 @@ public class ApplicationController extends BaseController {
*/ */
@PutMapping("/resetSecret/{appId}") @PutMapping("/resetSecret/{appId}")
public AjaxResult resetSecret(@PathVariable Long appId) { public AjaxResult resetSecret(@PathVariable Long appId) {
String newSecret = applicationService.resetAppSecret(appId); String newSecret = applicationService.resetSecret(appId);
return success(Map.of("appSecret", newSecret)); return success(newSecret);
} }
/** /**
* 获取API接口列表用于授权选择 * 获取API接口列表
*/ */
@GetMapping("/apiList") @GetMapping("/apiList")
public AjaxResult apiList() { public AjaxResult getApiList() {
List<ApiDict> list = applicationService.selectApiDictList(); List<ApiDict> list = applicationService.getApiList();
return success(list); return success(list);
} }
} }

View File

@ -3,12 +3,12 @@ package com.pangu.application.domain.dto;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
/** /**
* 应用传输对象 * 应用数据传输对象
*
* @author pangu * @author pangu
*/ */
@Data @Data
@ -16,38 +16,44 @@ public class ApplicationDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 应用ID(编辑时必填) */ /** 应用ID */
private Long appId; private Long appId;
/** 应用编码 */
private String appCode;
/** 应用名称 */ /** 应用名称 */
@NotBlank(message = "应用名称不能为空") @NotBlank(message = "应用名称不能为空")
@Size(max = 100, message = "应用名称不能超过100个字符")
private String appName; private String appName;
/** 应用编码(查询条件) */ /** 应用密钥 */
private String appCode; private String appSecret;
/** 应用描述 */
@Size(max = 500, message = "应用描述不能超过500个字符")
private String appDesc;
/** 联系人 */ /** 联系人 */
@Size(max = 50, message = "联系人不能超过50个字符")
private String contactPerson; private String contactPerson;
/** 联系电话 */ /** 联系电话 */
@Size(max = 20, message = "联系电话不能超过20个字符")
private String contactPhone; private String contactPhone;
/** 状态0正常 1停用 */ /** 状态0正常 1停用 */
private String status; private String status;
/** 授权的接口编码列表 */ /** 备注 */
private List<String> apiCodes; private String remark;
/** 分页页码 */ /** 授权接口ID列表 */
private List<Long> apiIds;
// ========== 查询条件 ==========
/** 开始时间(查询条件) */
private String beginTime;
/** 结束时间(查询条件) */
private String endTime;
/** 页码 */
private Integer pageNum; private Integer pageNum;
/** 分页大小 */ /** 每页数量 */
private Integer pageSize; private Integer pageSize;
} }

View File

@ -1,16 +1,14 @@
package com.pangu.application.domain.entity; package com.pangu.application.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* API接口字典实体 * API接口字典实体类
*
* @author pangu * @author pangu
*/ */
@Data @Data
@ -35,26 +33,16 @@ public class ApiDict implements Serializable {
/** 请求方法 */ /** 请求方法 */
private String apiMethod; private String apiMethod;
/** 接口描述 */ /** 排序 */
private String apiDesc; private Integer sortOrder;
/** 显示顺序 */
private Integer orderNum;
/** 状态0正常 1停用 */ /** 状态0正常 1停用 */
private String status; private String status;
/** 创建者 */
private String createBy;
/** 创建时间 */ /** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @TableField(fill = FieldFill.INSERT)
private Date createTime; private LocalDateTime createTime;
/** 更新者 */ /** 备注 */
private String updateBy; private String remark;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
} }

View File

@ -1,20 +1,18 @@
package com.pangu.application.domain.entity; package com.pangu.application.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
/** /**
* 应用接口授权实体 * 应用接口授权实体类
*
* @author pangu * @author pangu
*/ */
@Data @Data
@TableName("pg_app_grant") @TableName("pg_app_api")
public class AppApi implements Serializable { public class AppApi implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -26,19 +24,10 @@ public class AppApi implements Serializable {
/** 应用ID */ /** 应用ID */
private Long appId; private Long appId;
/** 接口编码 */ /** 接口ID */
private String apiCode; private Long apiId;
/** 接口名称 */
private String apiName;
/** 接口路径 */
private String apiPath;
/** 创建者 */
private String createBy;
/** 创建时间 */ /** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @TableField(fill = FieldFill.INSERT)
private Date createTime; private LocalDateTime createTime;
} }

View File

@ -1,14 +1,13 @@
package com.pangu.application.domain.entity; package com.pangu.application.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pangu.common.core.domain.BaseEntity; import com.pangu.common.core.domain.BaseEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
/** /**
* 应用实体 * 应用实体类
*
* @author pangu * @author pangu
*/ */
@Data @Data
@ -22,18 +21,15 @@ public class Application extends BaseEntity {
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long appId; private Long appId;
/** 应用编码格式YY + 6位序号 */ /** 应用编码 */
private String appCode; private String appCode;
/** 应用名称 */ /** 应用名称 */
private String appName; private String appName;
/** 应用密钥32位随机字符串 */ /** 应用密钥 */
private String appSecret; private String appSecret;
/** 应用描述 */
private String appDesc;
/** 联系人 */ /** 联系人 */
private String contactPerson; private String contactPerson;

View File

@ -4,11 +4,12 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
* 应用视图对象 * 应用视图对象
*
* @author pangu * @author pangu
*/ */
@Data @Data
@ -16,29 +17,57 @@ public class ApplicationVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 应用ID */
private Long appId; private Long appId;
/** 应用编码 */
private String appCode; private String appCode;
/** 应用名称 */
private String appName; private String appName;
private String appDesc;
/** 应用密钥 */
private String appSecret;
/** 联系人 */
private String contactPerson; private String contactPerson;
/** 联系电话 */
private String contactPhone; private String contactPhone;
/** 状态0正常 1停用 */
private String status; private String status;
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime; private LocalDateTime createTime;
/** 授权的接口列表 */ /** 备注 */
private List<ApiInfo> apis; private String remark;
/** 授权接口列表 */
private List<ApiVO> apiList;
/** /**
* 接口信息 * API视图对象
*/ */
@Data @Data
public static class ApiInfo implements Serializable { public static class ApiVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 接口ID */
private Long apiId;
/** 接口编码 */
private String apiCode; private String apiCode;
/** 接口名称 */
private String apiName; private String apiName;
/** 接口路径 */
private String apiPath; private String apiPath;
/** 请求方法 */
private String apiMethod;
} }
} }

View File

@ -3,26 +3,12 @@ package com.pangu.application.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pangu.application.domain.entity.ApiDict; import com.pangu.application.domain.entity.ApiDict;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/** /**
* API接口字典Mapper接口 * API接口字典Mapper接口
*
* @author pangu * @author pangu
*/ */
@Mapper @Mapper
public interface ApiDictMapper extends BaseMapper<ApiDict> { public interface ApiDictMapper extends BaseMapper<ApiDict> {
/**
* 查询启用的接口列表
*/
@Select("SELECT * FROM pg_api_dict WHERE status = '0' ORDER BY order_num")
List<ApiDict> selectEnabledList();
/**
* 根据接口编码列表查询
*/
List<ApiDict> selectByCodes(@Param("codes") List<String> codes);
} }

View File

@ -2,37 +2,32 @@ package com.pangu.application.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pangu.application.domain.entity.AppApi; import com.pangu.application.domain.entity.AppApi;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
/** /**
* 应用接口授权Mapper接口 * 应用接口授权Mapper接口
*
* @author pangu * @author pangu
*/ */
@Mapper @Mapper
public interface AppApiMapper extends BaseMapper<AppApi> { public interface AppApiMapper extends BaseMapper<AppApi> {
/** /**
* 根据应用ID查询授权接口 * 批量插入应用接口授权
*
* @param list 授权列表
* @return 影响行数
*/ */
@Select("SELECT * FROM pg_app_grant WHERE app_id = #{appId}") int batchInsert(@Param("list") List<AppApi> list);
List<AppApi> selectByAppId(@Param("appId") Long appId);
/** /**
* 根据应用ID删除授权接口 * 根据应用ID删除授权
*
* @param appId 应用ID
* @return 影响行数
*/ */
@Delete("DELETE FROM pg_app_grant WHERE app_id = #{appId}")
int deleteByAppId(@Param("appId") Long appId); int deleteByAppId(@Param("appId") Long appId);
/**
* 根据应用编码查询授权的接口路径列表
*/
@Select("SELECT aa.api_path FROM pg_app_grant aa "
+ "INNER JOIN pg_application a ON aa.app_id = a.app_id "
+ "WHERE a.app_code = #{appCode} AND a.del_flag = '0'")
List<String> selectApiPathsByAppCode(@Param("appCode") String appCode);
} }

View File

@ -1,33 +1,53 @@
package com.pangu.application.mapper; package com.pangu.application.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pangu.application.domain.dto.ApplicationDTO;
import com.pangu.application.domain.entity.Application; import com.pangu.application.domain.entity.Application;
import com.pangu.application.domain.vo.ApplicationVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/** /**
* 应用Mapper接口 * 应用Mapper接口
*
* @author pangu * @author pangu
*/ */
@Mapper @Mapper
public interface ApplicationMapper extends BaseMapper<Application> { public interface ApplicationMapper extends BaseMapper<Application> {
/** /**
* 查询最大应用编码 * 查询应用列表
*
* @param page 分页对象
* @param dto 查询条件
* @return 应用列表
*/ */
@Select("SELECT MAX(app_code) FROM pg_application WHERE del_flag = '0'") List<ApplicationVO> selectApplicationVOList(Page<ApplicationVO> page, @Param("dto") ApplicationDTO dto);
String selectMaxAppCode();
/** /**
* 根据应用编码查询应用 * 根据ID查询应用详情
*
* @param appId 应用ID
* @return 应用详情
*/ */
@Select("SELECT * FROM pg_application WHERE app_code = #{appCode} AND del_flag = '0'") ApplicationVO selectApplicationVOById(@Param("appId") Long appId);
Application selectByAppCode(@Param("appCode") String appCode);
/** /**
* 检查应用名称是否存在排除指定ID * 检查应用编码是否唯一
*
* @param appCode 应用编码
* @param appId 应用ID编辑时排除自己
* @return 数量
*/ */
@Select("SELECT COUNT(*) FROM pg_application WHERE app_name = #{appName} AND del_flag = '0' AND ( #{excludeId} IS NULL OR app_id != #{excludeId} )") int countByAppCode(@Param("appCode") String appCode, @Param("appId") Long appId);
int checkAppNameExists(@Param("appName") String appName, @Param("excludeId") Long excludeId);
/**
* 获取下一个应用编码序号
*
* @return 序号
*/
int getNextCodeSeq();
} }

View File

@ -1,63 +1,82 @@
package com.pangu.application.service; package com.pangu.application.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pangu.application.domain.dto.ApplicationDTO; import com.pangu.application.domain.dto.ApplicationDTO;
import com.pangu.application.domain.entity.ApiDict; import com.pangu.application.domain.entity.ApiDict;
import com.pangu.application.domain.entity.Application; import com.pangu.application.domain.entity.Application;
import com.pangu.application.domain.vo.ApplicationVO; import com.pangu.application.domain.vo.ApplicationVO;
import com.pangu.common.core.page.TableDataInfo;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 应用管理服务接口 * 应用服务接口
*
* @author pangu * @author pangu
*/ */
public interface IApplicationService { public interface IApplicationService extends IService<Application> {
/** /**
* 查询应用列表 * 查询应用列表
*
* @param applicationDTO 查询条件
* @return 应用列表
*/ */
List<ApplicationVO> selectApplicationList(ApplicationDTO dto); TableDataInfo selectApplicationList(ApplicationDTO applicationDTO);
/** /**
* 查询应用详情 * 根据ID查询应用详情
*
* @param appId 应用ID
* @return 应用详情
*/ */
ApplicationVO selectApplicationById(Long appId); ApplicationVO getApplicationById(Long appId);
/** /**
* 新增应用 * 新增应用
* @return 包含 appCode appSecret Map *
* @param applicationDTO 应用信息
* @return 结果包含新生成的密钥
*/ */
Map<String, String> insertApplication(ApplicationDTO dto); ApplicationVO insertApplication(ApplicationDTO applicationDTO);
/** /**
* 修改应用 * 修改应用
*
* @param applicationDTO 应用信息
* @return 结果
*/ */
int updateApplication(ApplicationDTO dto); int updateApplication(ApplicationDTO applicationDTO);
/** /**
* 删除应用 * 删除应用
*
* @param appId 应用ID
* @return 结果
*/ */
int deleteApplicationById(Long appId); int deleteApplication(Long appId);
/** /**
* 重置应用密钥 * 重置应用密钥
*
* @param appId 应用ID
* @return 新密钥 * @return 新密钥
*/ */
String resetAppSecret(Long appId); String resetSecret(Long appId);
/** /**
* 获取API接口字典列表用于授权选择 * 获取API接口列表
*
* @return API接口列表
*/ */
List<ApiDict> selectApiDictList(); List<ApiDict> getApiList();
/** /**
* 根据应用编码查询应用用于开放API认证 * 检查应用编码是否唯一
*
* @param appCode 应用编码
* @param appId 应用ID
* @return true唯一 false不唯一
*/ */
Application selectByAppCode(String appCode); boolean checkAppCodeUnique(String appCode, Long appId);
/**
* 检查应用是否有接口权限
*/
boolean checkApiPermission(String appCode, String apiPath);
} }

View File

@ -1,8 +1,10 @@
package com.pangu.application.service.impl; package com.pangu.application.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pangu.application.domain.dto.ApplicationDTO; import com.pangu.application.domain.dto.ApplicationDTO;
import com.pangu.application.domain.entity.ApiDict; import com.pangu.application.domain.entity.ApiDict;
import com.pangu.application.domain.entity.AppApi; import com.pangu.application.domain.entity.AppApi;
@ -13,222 +15,198 @@ import com.pangu.application.mapper.AppApiMapper;
import com.pangu.application.mapper.ApplicationMapper; import com.pangu.application.mapper.ApplicationMapper;
import com.pangu.application.service.IApplicationService; import com.pangu.application.service.IApplicationService;
import com.pangu.common.core.exception.ServiceException; import com.pangu.common.core.exception.ServiceException;
import com.pangu.common.utils.DateUtils; import com.pangu.common.core.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** /**
* 应用管理服务实现 * 应用服务实现
*
* @author pangu * @author pangu
*/ */
@Slf4j
@Service @Service
public class ApplicationServiceImpl implements IApplicationService { @RequiredArgsConstructor
public class ApplicationServiceImpl extends ServiceImpl<ApplicationMapper, Application> implements IApplicationService {
@Resource private final ApplicationMapper applicationMapper;
private ApplicationMapper applicationMapper; private final AppApiMapper appApiMapper;
@Resource private final ApiDictMapper apiDictMapper;
private AppApiMapper appApiMapper;
@Resource
private ApiDictMapper apiDictMapper;
@Override @Override
public List<ApplicationVO> selectApplicationList(ApplicationDTO dto) { public TableDataInfo selectApplicationList(ApplicationDTO applicationDTO) {
LambdaQueryWrapper<Application> wrapper = new LambdaQueryWrapper<>(); Page<ApplicationVO> page = new Page<>(
if (dto != null && dto.getAppName() != null && !dto.getAppName().isEmpty()) { applicationDTO.getPageNum() != null ? applicationDTO.getPageNum() : 1,
wrapper.like(Application::getAppName, dto.getAppName()); applicationDTO.getPageSize() != null ? applicationDTO.getPageSize() : 10
} );
if (dto != null && dto.getAppCode() != null && !dto.getAppCode().isEmpty()) { List<ApplicationVO> list = applicationMapper.selectApplicationVOList(page, applicationDTO);
wrapper.like(Application::getAppCode, dto.getAppCode()); return new TableDataInfo(list, page.getTotal());
}
if (dto != null && dto.getStatus() != null && !dto.getStatus().isEmpty()) {
wrapper.eq(Application::getStatus, dto.getStatus());
}
wrapper.orderByDesc(Application::getCreateTime);
List<Application> list = applicationMapper.selectList(wrapper);
List<ApplicationVO> result = new ArrayList<>();
for (Application app : list) {
result.add(toVO(app));
}
return result;
} }
@Override @Override
public ApplicationVO selectApplicationById(Long appId) { public ApplicationVO getApplicationById(Long appId) {
Application app = applicationMapper.selectById(appId); return applicationMapper.selectApplicationVOById(appId);
if (app == null) {
throw new ServiceException("应用不存在");
}
return toVO(app);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Map<String, String> insertApplication(ApplicationDTO dto) { public ApplicationVO insertApplication(ApplicationDTO applicationDTO) {
if (applicationMapper.checkAppNameExists(dto.getAppName(), null) > 0) { // 生成应用编码
throw new ServiceException("应用名称已存在");
}
String appCode = generateAppCode(); String appCode = generateAppCode();
// 生成应用密钥
String appSecret = generateAppSecret(); String appSecret = generateAppSecret();
Application app = new Application();
app.setAppCode(appCode); // 构建应用对象
app.setAppName(dto.getAppName()); Application application = buildApplication(applicationDTO);
app.setAppSecret(appSecret); application.setAppCode(appCode);
app.setAppDesc(dto.getAppDesc()); application.setAppSecret(appSecret);
app.setContactPerson(dto.getContactPerson()); application.setStatus("0");
app.setContactPhone(dto.getContactPhone());
app.setStatus(dto.getStatus() != null ? dto.getStatus() : "0"); // 保存应用
app.setCreateBy("admin"); int result = applicationMapper.insert(application);
app.setCreateTime(DateUtils.getNowDate()); if (result <= 0) {
app.setDelFlag("0"); throw new ServiceException("新增应用失败");
applicationMapper.insert(app); }
saveAppApis(app.getAppId(), dto.getApiCodes());
Map<String, String> result = new HashMap<>(); // 保存接口授权
result.put("appCode", appCode); saveAppApis(application.getAppId(), applicationDTO.getApiIds());
result.put("appSecret", appSecret);
log.info("新增应用成功, appId={}, appCode={}", application.getAppId(), appCode);
// 返回应用信息包含密钥
ApplicationVO vo = new ApplicationVO();
vo.setAppId(application.getAppId());
vo.setAppCode(appCode);
vo.setAppName(application.getAppName());
vo.setAppSecret(appSecret);
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateApplication(ApplicationDTO applicationDTO) {
// 构建应用对象
Application application = buildApplication(applicationDTO);
application.setAppId(applicationDTO.getAppId());
// 更新应用
int result = applicationMapper.updateById(application);
if (result <= 0) {
throw new ServiceException("修改应用失败");
}
// 更新接口授权
appApiMapper.deleteByAppId(application.getAppId());
saveAppApis(application.getAppId(), applicationDTO.getApiIds());
log.info("修改应用成功, appId={}, appName={}", application.getAppId(), application.getAppName());
return result; return result;
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int updateApplication(ApplicationDTO dto) { public int deleteApplication(Long appId) {
if (dto.getAppId() == null) { // 软删除应用
throw new ServiceException("应用ID不能为空"); Application application = new Application();
} application.setAppId(appId);
Application existApp = applicationMapper.selectById(dto.getAppId()); application.setDelFlag("1");
if (existApp == null) { int result = applicationMapper.updateById(application);
throw new ServiceException("应用不存在");
}
if (applicationMapper.checkAppNameExists(dto.getAppName(), dto.getAppId()) > 0) {
throw new ServiceException("应用名称已存在");
}
Application app = new Application();
app.setAppId(dto.getAppId());
app.setAppName(dto.getAppName());
app.setAppDesc(dto.getAppDesc());
app.setContactPerson(dto.getContactPerson());
app.setContactPhone(dto.getContactPhone());
app.setStatus(dto.getStatus());
app.setUpdateBy("admin");
app.setUpdateTime(DateUtils.getNowDate());
int rows = applicationMapper.updateById(app);
appApiMapper.deleteByAppId(dto.getAppId());
saveAppApis(dto.getAppId(), dto.getApiCodes());
return rows;
}
@Override // 删除接口授权
@Transactional(rollbackFor = Exception.class)
public int deleteApplicationById(Long appId) {
Application app = applicationMapper.selectById(appId);
if (app == null) {
throw new ServiceException("应用不存在");
}
int rows = applicationMapper.deleteById(appId);
appApiMapper.deleteByAppId(appId); appApiMapper.deleteByAppId(appId);
return rows;
log.info("删除应用成功, appId={}", appId);
return result;
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public String resetAppSecret(Long appId) { public String resetSecret(Long appId) {
Application app = applicationMapper.selectById(appId); // 生成新密钥
if (app == null) {
throw new ServiceException("应用不存在");
}
String newSecret = generateAppSecret(); String newSecret = generateAppSecret();
Application updateApp = new Application();
updateApp.setAppId(appId); // 更新密钥
updateApp.setAppSecret(newSecret); Application application = new Application();
updateApp.setUpdateBy("admin"); application.setAppId(appId);
updateApp.setUpdateTime(DateUtils.getNowDate()); application.setAppSecret(newSecret);
applicationMapper.updateById(updateApp); int result = applicationMapper.updateById(application);
if (result <= 0) {
throw new ServiceException("重置密钥失败");
}
log.info("重置应用密钥成功, appId={}", appId);
return newSecret; return newSecret;
} }
@Override @Override
public List<ApiDict> selectApiDictList() { public List<ApiDict> getApiList() {
return apiDictMapper.selectEnabledList(); LambdaQueryWrapper<ApiDict> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ApiDict::getStatus, "0");
wrapper.orderByAsc(ApiDict::getSortOrder);
return apiDictMapper.selectList(wrapper);
} }
@Override @Override
public Application selectByAppCode(String appCode) { public boolean checkAppCodeUnique(String appCode, Long appId) {
return applicationMapper.selectByAppCode(appCode); int count = applicationMapper.countByAppCode(appCode, appId);
} return count == 0;
@Override
public boolean checkApiPermission(String appCode, String apiPath) {
List<String> paths = appApiMapper.selectApiPathsByAppCode(appCode);
return paths != null && paths.contains(apiPath);
} }
/**
* 生成应用编码
* 格式YY + 6位序号
*/
private String generateAppCode() { private String generateAppCode() {
String maxCode = applicationMapper.selectMaxAppCode(); int seq = applicationMapper.getNextCodeSeq();
int nextSeq = 1; return String.format("YY%06d", seq);
if (maxCode != null && maxCode.length() > 2) {
try {
nextSeq = Integer.parseInt(maxCode.substring(2)) + 1;
} catch (NumberFormatException ignored) {
}
}
return String.format("YY%06d", nextSeq);
} }
/**
* 生成应用密钥
* 32位随机字符串
*/
private String generateAppSecret() { private String generateAppSecret() {
return RandomUtil.randomString(32); return RandomUtil.randomString(32);
} }
private void saveAppApis(Long appId, List<String> apiCodes) { /**
if (CollUtil.isEmpty(apiCodes)) { * 构建应用对象
return; */
} private Application buildApplication(ApplicationDTO dto) {
List<ApiDict> apiDicts = apiDictMapper.selectByCodes(apiCodes); Application application = new Application();
Map<String, ApiDict> dictMap = apiDicts.stream().collect(Collectors.toMap(ApiDict::getApiCode, d -> d)); application.setAppName(dto.getAppName());
String username = "admin"; application.setContactPerson(dto.getContactPerson());
java.util.Date now = DateUtils.getNowDate(); application.setContactPhone(dto.getContactPhone());
for (String apiCode : apiCodes) { application.setRemark(dto.getRemark());
ApiDict dict = dictMap.get(apiCode); return application;
if (dict != null) {
AppApi appApi = new AppApi();
appApi.setAppId(appId);
appApi.setApiCode(apiCode);
appApi.setApiName(dict.getApiName());
appApi.setApiPath(dict.getApiPath());
appApi.setCreateBy(username);
appApi.setCreateTime(now);
appApiMapper.insert(appApi);
}
}
} }
private ApplicationVO toVO(Application app) { /**
ApplicationVO vo = new ApplicationVO(); * 保存接口授权
vo.setAppId(app.getAppId()); */
vo.setAppCode(app.getAppCode()); private void saveAppApis(Long appId, List<Long> apiIds) {
vo.setAppName(app.getAppName()); if (apiIds == null || apiIds.isEmpty()) {
vo.setAppDesc(app.getAppDesc()); return;
vo.setContactPerson(app.getContactPerson());
vo.setContactPhone(app.getContactPhone());
vo.setStatus(app.getStatus());
vo.setCreateBy(app.getCreateBy());
vo.setCreateTime(app.getCreateTime());
List<AppApi> appApis = appApiMapper.selectByAppId(app.getAppId());
if (CollUtil.isNotEmpty(appApis)) {
List<ApplicationVO.ApiInfo> apis = appApis.stream().map(api -> {
ApplicationVO.ApiInfo info = new ApplicationVO.ApiInfo();
info.setApiCode(api.getApiCode());
info.setApiName(api.getApiName());
info.setApiPath(api.getApiPath());
return info;
}).collect(Collectors.toList());
vo.setApis(apis);
} }
return vo;
List<AppApi> list = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
for (Long apiId : apiIds) {
AppApi appApi = new AppApi();
appApi.setAppId(appId);
appApi.setApiId(apiId);
appApi.setCreateTime(now);
list.add(appApi);
}
appApiMapper.batchInsert(list);
} }
} }

View File

@ -44,4 +44,11 @@ public interface IRegionService {
* 是否存在子区域 * 是否存在子区域
*/ */
boolean hasChildRegion(Long regionId); boolean hasChildRegion(Long regionId);
/**
* 根据区域路径查询区域ID
* @param regionPath 区域路径湖北省-武汉市-武昌区
* @return 区域ID
*/
Long getRegionIdByPath(String regionPath);
} }

View File

@ -127,4 +127,29 @@ public class RegionServiceImpl implements IRegionService {
int count = regionMapper.countChildByParentId(regionId); int count = regionMapper.countChildByParentId(regionId);
return count > 0; return count > 0;
} }
@Override
public Long getRegionIdByPath(String regionPath) {
if (StringUtils.isEmpty(regionPath)) {
return null;
}
// 解析区域路径湖北省-武汉市-武昌区
String[] parts = regionPath.split("-");
if (parts.length == 0) {
return null;
}
// 从最后一级开始查询 -> ->
String regionName = parts[parts.length - 1];
List<Region> regions = regionMapper.selectRegionList(new Region());
for (Region region : regions) {
if (regionName.equals(region.getRegionName())) {
return region.getRegionId();
}
}
return null;
}
} }

View File

@ -106,4 +106,11 @@ public interface IMemberService extends IService<Member> {
* @return 会员信息 * @return 会员信息
*/ */
Member getMemberByOpenId(String openId); Member getMemberByOpenId(String openId);
/**
* 根据手机号查询或创建会员
* @param phone 手机号
* @return 会员ID
*/
Long getOrCreateMemberByPhone(String phone);
} }

View File

@ -34,6 +34,7 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
private final MemberMapper memberMapper; private final MemberMapper memberMapper;
private final BCryptPasswordEncoder passwordEncoder; private final BCryptPasswordEncoder passwordEncoder;
private final com.pangu.student.service.IStudentService studentService;
/** 默认密码 */ /** 默认密码 */
private static final String DEFAULT_PASSWORD = "123456"; private static final String DEFAULT_PASSWORD = "123456";
@ -68,8 +69,9 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
// 填充名称 // 填充名称
fillMemberVONames(memberVO); fillMemberVONames(memberVO);
// 查询绑定的学生暂时返回空列表等学生模块完成后再实现 // 查询绑定的学生
memberVO.setStudents(new ArrayList<>()); List<com.pangu.student.domain.vo.StudentVO> students = studentService.selectStudentVOsByMemberId(memberId);
memberVO.setStudents(students);
return memberVO; return memberVO;
} }
@ -194,24 +196,26 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
throw new ServiceException("会员不存在"); throw new ServiceException("会员不存在");
} }
// TODO: 教师只能绑定本校学生等学生模块完成后实现 // 教师只能绑定本校学生
// if (IdentityTypeEnum.isTeacher(member.getIdentityType())) { if (IdentityTypeEnum.isTeacher(member.getIdentityType())) {
// if (!studentService.isStudentInSchool(studentId, member.getSchoolId())) { if (!studentService.isStudentInSchool(studentId, member.getSchoolId())) {
// throw new ServiceException("教师只能绑定本校学生"); throw new ServiceException("教师只能绑定本校学生");
// } }
// } }
// TODO: 调用学生模块的接口更新学生的memberId等学生模块完成后实现 // 更新学生的memberId
int result = studentService.updateStudentMember(studentId, memberId);
log.info("绑定学生成功, memberId={}, studentId={}", memberId, studentId); log.info("绑定学生成功, memberId={}, studentId={}", memberId, studentId);
return 1; return result;
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int unbindStudent(Long memberId, Long studentId) { public int unbindStudent(Long memberId, Long studentId) {
// TODO: 调用学生模块的接口解绑等学生模块完成后实现 // 解绑学生
int result = studentService.unbindStudent(studentId);
log.info("解绑学生成功, memberId={}, studentId={}", memberId, studentId); log.info("解绑学生成功, memberId={}, studentId={}", memberId, studentId);
return 1; return result;
} }
@Override @Override
@ -221,9 +225,8 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
@Override @Override
public boolean checkCanDelete(Long memberId) { public boolean checkCanDelete(Long memberId) {
// TODO: 检查是否有绑定的学生等学生模块完成后实现 // 检查是否有绑定的学生
// return studentService.countByMemberId(memberId) == 0; return studentService.countByMemberId(memberId) == 0;
return true;
} }
@Override @Override
@ -312,4 +315,30 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
default -> "未知"; default -> "未知";
}; };
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Long getOrCreateMemberByPhone(String phone) {
// 先查询是否存在
Member existMember = getMemberByPhone(phone);
if (existMember != null) {
return existMember.getMemberId();
}
// 不存在则创建
Member newMember = new Member();
newMember.setMemberCode(generateMemberCode());
newMember.setPhone(phone);
newMember.setNickname(generateNickname(phone));
newMember.setPassword(passwordEncoder.encode(DEFAULT_PASSWORD));
newMember.setIdentityType(IdentityTypeEnum.PARENT.getCode());
newMember.setRegisterSource(RegisterSourceEnum.BACKEND.getCode());
newMember.setStatus("0");
newMember.setCreateTime(LocalDateTime.now());
memberMapper.insert(newMember);
log.info("批量导入自动创建会员, memberId={}, phone={}", newMember.getMemberId(), phone);
return newMember.getMemberId();
}
} }

View File

@ -85,4 +85,28 @@ public interface ISchoolService {
* @return 影响行数 * @return 影响行数
*/ */
int deleteSchoolClass(Long schoolClassId); int deleteSchoolClass(Long schoolClassId);
/**
* 根据学校名称和区域ID查询学校ID
* @param schoolName 学校名称
* @param regionId 区域ID
* @return 学校ID
*/
Long getSchoolIdByName(String schoolName, Long regionId);
/**
* 根据学校ID和年级名称查询学校年级ID
* @param schoolId 学校ID
* @param gradeName 年级名称
* @return 学校年级关联ID
*/
Long getSchoolGradeId(Long schoolId, String gradeName);
/**
* 根据学校年级ID和班级名称查询学校班级ID
* @param schoolGradeId 学校年级ID
* @param className 班级名称
* @return 学校班级关联ID
*/
Long getSchoolClassId(Long schoolGradeId, String className);
} }

View File

@ -313,4 +313,35 @@ public class SchoolServiceImpl implements ISchoolService {
return vo; return vo;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@Override
public Long getSchoolIdByName(String schoolName, Long regionId) {
SchoolQueryDTO query = new SchoolQueryDTO();
query.setSchoolName(schoolName);
query.setRegionId(regionId);
List<SchoolVO> schools = schoolMapper.selectSchoolList(query);
return schools.isEmpty() ? null : schools.get(0).getSchoolId();
}
@Override
public Long getSchoolGradeId(Long schoolId, String gradeName) {
List<SchoolGrade> grades = schoolGradeMapper.selectBySchoolIds(Collections.singletonList(schoolId));
for (SchoolGrade grade : grades) {
if (gradeName.equals(grade.getGradeName())) {
return grade.getId();
}
}
return null;
}
@Override
public Long getSchoolClassId(Long schoolGradeId, String className) {
List<SchoolClass> classes = schoolClassMapper.selectBySchoolGradeIds(Collections.singletonList(schoolGradeId));
for (SchoolClass clazz : classes) {
if (className.equals(clazz.getClassName())) {
return clazz.getId();
}
}
return null;
}
} }

View File

@ -1,9 +1,11 @@
package com.pangu.student.controller; package com.pangu.student.controller;
import com.alibaba.excel.EasyExcel;
import com.pangu.common.core.controller.BaseController; import com.pangu.common.core.controller.BaseController;
import com.pangu.common.core.domain.AjaxResult; import com.pangu.common.core.domain.AjaxResult;
import com.pangu.common.core.page.TableDataInfo; import com.pangu.common.core.page.TableDataInfo;
import com.pangu.student.domain.dto.StudentDTO; import com.pangu.student.domain.dto.StudentDTO;
import com.pangu.student.domain.dto.StudentImportDTO;
import com.pangu.student.domain.vo.ImportResultVO; import com.pangu.student.domain.vo.ImportResultVO;
import com.pangu.student.domain.vo.StudentVO; import com.pangu.student.domain.vo.StudentVO;
import com.pangu.student.service.IStudentService; import com.pangu.student.service.IStudentService;
@ -12,6 +14,13 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/** /**
* 学生管理Controller * 学生管理Controller
* *
@ -78,8 +87,29 @@ public class StudentController extends BaseController {
* 下载导入模板 * 下载导入模板
*/ */
@GetMapping("/template") @GetMapping("/template")
public void downloadTemplate() { public void downloadTemplate(HttpServletResponse response) throws IOException {
// TODO: 实现模板下载 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生导入模板", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 创建模板数据
List<StudentImportDTO> templateData = new ArrayList<>();
StudentImportDTO example = new StudentImportDTO();
example.setStudentName("张小明");
example.setStudentNo("STU20260001");
example.setMemberPhone("13812345678");
example.setRegionPath("湖北省-武汉市-武昌区");
example.setSchoolName("武汉市第一中学");
example.setGradeName("七年级");
example.setClassName("1班");
example.setGender("");
example.setBirthday("2015-03");
templateData.add(example);
EasyExcel.write(response.getOutputStream(), StudentImportDTO.class)
.sheet("学生信息")
.doWrite(templateData);
} }
/** /**

View File

@ -3,11 +3,18 @@ package com.pangu.student.listener;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.excel.event.AnalysisEventListener;
import com.pangu.base.service.IRegionService;
import com.pangu.member.service.IMemberService;
import com.pangu.school.service.ISchoolService;
import com.pangu.student.domain.dto.StudentDTO;
import com.pangu.student.domain.dto.StudentImportDTO; import com.pangu.student.domain.dto.StudentImportDTO;
import com.pangu.student.domain.vo.ImportResultVO; import com.pangu.student.domain.vo.ImportResultVO;
import com.pangu.student.service.IStudentService; import com.pangu.student.service.IStudentService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/** /**
* 学生导入监听器 * 学生导入监听器
* *
@ -17,11 +24,20 @@ import lombok.extern.slf4j.Slf4j;
public class StudentImportListener extends AnalysisEventListener<StudentImportDTO> { public class StudentImportListener extends AnalysisEventListener<StudentImportDTO> {
private final IStudentService studentService; private final IStudentService studentService;
private final IRegionService regionService;
private final ISchoolService schoolService;
private final IMemberService memberService;
private final ImportResultVO result = new ImportResultVO(); private final ImportResultVO result = new ImportResultVO();
private int rowNum = 1; // 从第2行开始第1行是表头 private int rowNum = 1; // 从第2行开始第1行是表头
public StudentImportListener(IStudentService studentService) { public StudentImportListener(IStudentService studentService,
IRegionService regionService,
ISchoolService schoolService,
IMemberService memberService) {
this.studentService = studentService; this.studentService = studentService;
this.regionService = regionService;
this.schoolService = schoolService;
this.memberService = memberService;
result.setTotal(0); result.setTotal(0);
result.setSuccessCount(0); result.setSuccessCount(0);
result.setFailCount(0); result.setFailCount(0);
@ -41,15 +57,60 @@ public class StudentImportListener extends AnalysisEventListener<StudentImportDT
return; return;
} }
// TODO: 实际导入逻辑需要查询区域学校会员等信息
// 这里简化处理实际需要
// 1. 根据区域路径查询区域ID // 1. 根据区域路径查询区域ID
// 2. 根据学校名称查询学校ID Long regionId = regionService.getRegionIdByPath(data.getRegionPath());
// 3. 根据年级名称查询学校年级ID if (regionId == null) {
// 4. 根据班级名称查询学校班级ID addError(rowNum, data, "区域不存在:" + data.getRegionPath());
// 5. 根据手机号查询或创建会员 result.setFailCount(result.getFailCount() + 1);
// 6. 保存学生信息 return;
}
// 2. 根据学校名称查询学校ID
Long schoolId = schoolService.getSchoolIdByName(data.getSchoolName(), regionId);
if (schoolId == null) {
addError(rowNum, data, "学校不存在:" + data.getSchoolName());
result.setFailCount(result.getFailCount() + 1);
return;
}
// 3. 根据年级名称查询学校年级ID
Long schoolGradeId = schoolService.getSchoolGradeId(schoolId, data.getGradeName());
if (schoolGradeId == null) {
addError(rowNum, data, "年级不存在:" + data.getGradeName());
result.setFailCount(result.getFailCount() + 1);
return;
}
// 4. 根据班级名称查询学校班级ID
Long schoolClassId = schoolService.getSchoolClassId(schoolGradeId, data.getClassName());
if (schoolClassId == null) {
addError(rowNum, data, "班级不存在:" + data.getClassName());
result.setFailCount(result.getFailCount() + 1);
return;
}
// 5. 根据手机号查询或创建会员
Long memberId = memberService.getOrCreateMemberByPhone(data.getMemberPhone());
if (memberId == null) {
addError(rowNum, data, "会员创建失败");
result.setFailCount(result.getFailCount() + 1);
return;
}
// 6. 保存学生信息
StudentDTO studentDTO = new StudentDTO();
studentDTO.setStudentName(data.getStudentName());
studentDTO.setStudentNo(data.getStudentNo());
studentDTO.setGender(parseGender(data.getGender()));
studentDTO.setBirthday(parseBirthday(data.getBirthday()));
studentDTO.setRegionId(regionId);
studentDTO.setRegionPath(data.getRegionPath());
studentDTO.setSchoolId(schoolId);
studentDTO.setSchoolGradeId(schoolGradeId);
studentDTO.setSchoolClassId(schoolClassId);
studentDTO.setMemberId(memberId);
studentService.insertStudent(studentDTO);
result.setSuccessCount(result.getSuccessCount() + 1); result.setSuccessCount(result.getSuccessCount() + 1);
log.info("导入学生成功:{}", data.getStudentName()); log.info("导入学生成功:{}", data.getStudentName());
@ -109,4 +170,34 @@ public class StudentImportListener extends AnalysisEventListener<StudentImportDT
public ImportResultVO getResult() { public ImportResultVO getResult() {
return result; return result;
} }
/**
* 解析性别
*/
private String parseGender(String gender) {
if (StrUtil.isBlank(gender)) {
return "0";
}
return switch (gender) {
case "" -> "1";
case "" -> "2";
default -> "0";
};
}
/**
* 解析出生年月
*/
private LocalDate parseBirthday(String birthday) {
if (StrUtil.isBlank(birthday)) {
return null;
}
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
return LocalDate.parse(birthday + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} catch (Exception e) {
log.warn("解析出生年月失败:{}", birthday);
return null;
}
}
} }

View File

@ -43,4 +43,20 @@ public interface StudentMapper extends BaseMapper<Student> {
* @return 数量 * @return 数量
*/ */
int countByStudentNo(@Param("studentNo") String studentNo, @Param("studentId") Long studentId); int countByStudentNo(@Param("studentNo") String studentNo, @Param("studentId") Long studentId);
/**
* 根据会员ID统计学生数量
*
* @param memberId 会员ID
* @return 学生数量
*/
int countByMemberId(@Param("memberId") Long memberId);
/**
* 根据会员ID查询学生列表
*
* @param memberId 会员ID
* @return 学生列表
*/
List<StudentVO> selectStudentVOsByMemberId(@Param("memberId") Long memberId);
} }

View File

@ -71,4 +71,46 @@ public interface IStudentService extends IService<Student> {
* @return true唯一 false不唯一 * @return true唯一 false不唯一
*/ */
boolean checkStudentNoUnique(String studentNo, Long studentId); boolean checkStudentNoUnique(String studentNo, Long studentId);
/**
* 检查学生是否在指定学校
*
* @param studentId 学生ID
* @param schoolId 学校ID
* @return true在该学校 false不在
*/
boolean isStudentInSchool(Long studentId, Long schoolId);
/**
* 更新学生的会员ID
*
* @param studentId 学生ID
* @param memberId 会员ID
* @return 结果
*/
int updateStudentMember(Long studentId, Long memberId);
/**
* 解绑学生清空会员ID
*
* @param studentId 学生ID
* @return 结果
*/
int unbindStudent(Long studentId);
/**
* 统计会员绑定的学生数量
*
* @param memberId 会员ID
* @return 学生数量
*/
int countByMemberId(Long memberId);
/**
* 根据会员ID查询学生列表
*
* @param memberId 会员ID
* @return 学生列表
*/
List<StudentVO> selectStudentVOsByMemberId(Long memberId);
} }

View File

@ -34,6 +34,9 @@ import java.util.List;
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements IStudentService { public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements IStudentService {
private final StudentMapper studentMapper; private final StudentMapper studentMapper;
private final com.pangu.base.service.IRegionService regionService;
private final com.pangu.school.service.ISchoolService schoolService;
private final com.pangu.member.service.IMemberService memberService;
@Override @Override
public TableDataInfo selectStudentList(StudentDTO studentDTO) { public TableDataInfo selectStudentList(StudentDTO studentDTO) {
@ -99,7 +102,7 @@ public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> impl
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public ImportResultVO importStudents(MultipartFile file) { public ImportResultVO importStudents(MultipartFile file) {
try { try {
StudentImportListener listener = new StudentImportListener(this); StudentImportListener listener = new StudentImportListener(this, regionService, schoolService, memberService);
EasyExcel.read(file.getInputStream(), StudentImportDTO.class, listener).sheet().doRead(); EasyExcel.read(file.getInputStream(), StudentImportDTO.class, listener).sheet().doRead();
return listener.getResult(); return listener.getResult();
} catch (IOException e) { } catch (IOException e) {
@ -114,6 +117,47 @@ public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> impl
return count == 0; return count == 0;
} }
@Override
public boolean isStudentInSchool(Long studentId, Long schoolId) {
Student student = studentMapper.selectById(studentId);
if (student == null || "1".equals(student.getDelFlag())) {
return false;
}
return student.getSchoolId().equals(schoolId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateStudentMember(Long studentId, Long memberId) {
Student student = new Student();
student.setStudentId(studentId);
student.setMemberId(memberId);
int result = studentMapper.updateById(student);
log.info("更新学生会员关联成功, studentId={}, memberId={}", studentId, memberId);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int unbindStudent(Long studentId) {
Student student = new Student();
student.setStudentId(studentId);
student.setMemberId(null);
int result = studentMapper.updateById(student);
log.info("解绑学生成功, studentId={}", studentId);
return result;
}
@Override
public int countByMemberId(Long memberId) {
return studentMapper.countByMemberId(memberId);
}
@Override
public List<StudentVO> selectStudentVOsByMemberId(Long memberId) {
return studentMapper.selectStudentVOsByMemberId(memberId);
}
/** /**
* 构建学生对象 * 构建学生对象
*/ */

View File

@ -0,0 +1,21 @@
<?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="com.pangu.application.mapper.AppApiMapper">
<!-- 批量插入应用接口授权 -->
<insert id="batchInsert">
INSERT INTO pg_app_api (app_id, api_id, create_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.appId}, #{item.apiId}, #{item.createTime})
</foreach>
</insert>
<!-- 根据应用ID删除授权 -->
<delete id="deleteByAppId">
DELETE FROM pg_app_api WHERE app_id = #{appId}
</delete>
</mapper>

View File

@ -0,0 +1,108 @@
<?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="com.pangu.application.mapper.ApplicationMapper">
<!-- 应用VO结果映射 -->
<resultMap id="ApplicationVOResult" type="com.pangu.application.domain.vo.ApplicationVO">
<id property="appId" column="app_id"/>
<result property="appCode" column="app_code"/>
<result property="appName" column="app_name"/>
<result property="appSecret" column="app_secret"/>
<result property="contactPerson" column="contact_person"/>
<result property="contactPhone" column="contact_phone"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="remark" column="remark"/>
<collection property="apiList" ofType="com.pangu.application.domain.vo.ApplicationVO$ApiVO">
<id property="apiId" column="api_id"/>
<result property="apiCode" column="api_code"/>
<result property="apiName" column="api_name"/>
<result property="apiPath" column="api_path"/>
<result property="apiMethod" column="api_method"/>
</collection>
</resultMap>
<!-- 查询应用列表 -->
<select id="selectApplicationVOList" resultMap="ApplicationVOResult">
SELECT DISTINCT
a.app_id,
a.app_code,
a.app_name,
a.app_secret,
a.contact_person,
a.contact_phone,
a.status,
a.create_time,
a.remark,
api.api_id,
api.api_code,
api.api_name,
api.api_path,
api.api_method
FROM pg_application a
LEFT JOIN pg_app_api aa ON a.app_id = aa.app_id
LEFT JOIN pg_api_dict api ON aa.api_id = api.api_id AND api.status = '0'
WHERE a.del_flag = '0'
<if test="dto.appName != null and dto.appName != ''">
AND a.app_name LIKE CONCAT('%', #{dto.appName}, '%')
</if>
<if test="dto.appCode != null and dto.appCode != ''">
AND a.app_code = #{dto.appCode}
</if>
<if test="dto.status != null and dto.status != ''">
AND a.status = #{dto.status}
</if>
<if test="dto.beginTime != null and dto.beginTime != ''">
AND DATE_FORMAT(a.create_time,'%Y-%m-%d') &gt;= #{dto.beginTime}
</if>
<if test="dto.endTime != null and dto.endTime != ''">
AND DATE_FORMAT(a.create_time,'%Y-%m-%d') &lt;= #{dto.endTime}
</if>
ORDER BY a.create_time DESC
</select>
<!-- 根据ID查询应用详情 -->
<select id="selectApplicationVOById" resultMap="ApplicationVOResult">
SELECT
a.app_id,
a.app_code,
a.app_name,
a.app_secret,
a.contact_person,
a.contact_phone,
a.status,
a.create_time,
a.remark,
api.api_id,
api.api_code,
api.api_name,
api.api_path,
api.api_method
FROM pg_application a
LEFT JOIN pg_app_api aa ON a.app_id = aa.app_id
LEFT JOIN pg_api_dict api ON aa.api_id = api.api_id AND api.status = '0'
WHERE a.app_id = #{appId} AND a.del_flag = '0'
</select>
<!-- 检查应用编码是否唯一 -->
<select id="countByAppCode" resultType="int">
SELECT COUNT(1)
FROM pg_application
WHERE app_code = #{appCode}
AND del_flag = '0'
<if test="appId != null">
AND app_id != #{appId}
</if>
</select>
<!-- 获取下一个应用编码序号 -->
<select id="getNextCodeSeq" resultType="int">
SELECT COALESCE(MAX(CAST(SUBSTRING(app_code, 3) AS UNSIGNED)), 0) + 1
FROM pg_application
WHERE app_code LIKE 'YY%'
AND del_flag = '0'
</select>
</mapper>

View File

@ -152,4 +152,47 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if> </if>
</select> </select>
<!-- 根据会员ID统计学生数量 -->
<select id="countByMemberId" resultType="int">
SELECT COUNT(1)
FROM pg_student
WHERE member_id = #{memberId}
AND del_flag = '0'
</select>
<!-- 根据会员ID查询学生列表 -->
<select id="selectStudentVOsByMemberId" resultMap="StudentVOResult">
SELECT
s.student_id,
s.student_name,
s.student_no,
s.gender,
CASE s.gender
WHEN '1' THEN '男'
WHEN '2' THEN '女'
ELSE '未知'
END AS gender_name,
s.birthday,
s.region_id,
s.region_path,
s.school_id,
sch.school_name,
s.school_grade_id,
g.grade_name,
s.school_class_id,
c.class_name,
s.subject_id,
sub.subject_name,
s.create_time
FROM pg_student s
LEFT JOIN pg_school sch ON s.school_id = sch.school_id AND sch.del_flag = '0'
LEFT JOIN pg_school_grade sg ON s.school_grade_id = sg.id AND sg.del_flag = '0'
LEFT JOIN pg_grade g ON sg.grade_id = g.grade_id AND g.del_flag = '0'
LEFT JOIN pg_school_class sc ON s.school_class_id = sc.id AND sc.del_flag = '0'
LEFT JOIN pg_class c ON sc.class_id = c.class_id AND c.del_flag = '0'
LEFT JOIN pg_subject sub ON s.subject_id = sub.subject_id AND sub.del_flag = '0'
WHERE s.member_id = #{memberId} AND s.del_flag = '0'
ORDER BY s.create_time DESC
</select>
</mapper> </mapper>

View File

@ -1,16 +1,18 @@
-- ============================================================ -- ============================================================
-- 盘古用户平台 - 应用管理模块表结构及初始数据 -- 应用管理模块 - 数据库脚本
-- 作者:湖北新华业务中台研发团队 -- 作者:pangu
-- 说明:执行前请确认已存在 sys_menu菜单已在 pangu_menu.sql 中 -- 创建时间2026-01-31
-- ============================================================ -- ============================================================
-- ----------------------------
-- 应用表 -- 应用表
CREATE TABLE IF NOT EXISTS `pg_application` ( -- ----------------------------
DROP TABLE IF EXISTS `pg_application`;
CREATE TABLE `pg_application` (
`app_id` bigint NOT NULL AUTO_INCREMENT COMMENT '应用ID', `app_id` bigint NOT NULL AUTO_INCREMENT COMMENT '应用ID',
`app_code` varchar(32) NOT NULL COMMENT '应用编码', `app_code` varchar(32) NOT NULL COMMENT '应用编码',
`app_name` varchar(100) NOT NULL COMMENT '应用名称', `app_name` varchar(100) NOT NULL COMMENT '应用名称',
`app_secret` varchar(64) NOT NULL COMMENT '应用密钥', `app_secret` varchar(64) NOT NULL COMMENT '应用密钥',
`app_desc` varchar(500) DEFAULT NULL COMMENT '应用描述',
`contact_person` varchar(50) DEFAULT NULL COMMENT '联系人', `contact_person` varchar(50) DEFAULT NULL COMMENT '联系人',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`status` char(1) DEFAULT '0' COMMENT '状态0正常 1停用', `status` char(1) DEFAULT '0' COMMENT '状态0正常 1停用',
@ -21,47 +23,67 @@ CREATE TABLE IF NOT EXISTS `pg_application` (
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志0存在 1删除', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志0存在 1删除',
`remark` varchar(500) DEFAULT NULL COMMENT '备注', `remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`app_id`), PRIMARY KEY (`app_id`),
UNIQUE KEY `uk_app_code` (`app_code`), UNIQUE KEY `uk_app_code` (`app_code`)
UNIQUE KEY `uk_app_name` (`app_name`, `del_flag`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应用表';
-- 应用接口授权表(记录应用被授予的可调用接口)
CREATE TABLE IF NOT EXISTS `pg_app_grant` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`app_id` bigint NOT NULL COMMENT '应用ID',
`api_code` varchar(100) NOT NULL COMMENT '接口编码',
`api_name` varchar(100) DEFAULT NULL COMMENT '接口名称',
`api_path` varchar(200) NOT NULL COMMENT '接口路径',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_app_api` (`app_id`, `api_code`),
KEY `idx_app_id` (`app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应用接口授权表';
-- ----------------------------
-- API接口字典表 -- API接口字典表
CREATE TABLE IF NOT EXISTS `pg_api_dict` ( -- ----------------------------
DROP TABLE IF EXISTS `pg_api_dict`;
CREATE TABLE `pg_api_dict` (
`api_id` bigint NOT NULL AUTO_INCREMENT COMMENT '接口ID', `api_id` bigint NOT NULL AUTO_INCREMENT COMMENT '接口ID',
`api_code` varchar(100) NOT NULL COMMENT '接口编码', `api_code` varchar(50) NOT NULL COMMENT '接口编码',
`api_name` varchar(100) NOT NULL COMMENT '接口名称', `api_name` varchar(100) NOT NULL COMMENT '接口名称',
`api_path` varchar(200) NOT NULL COMMENT '接口路径', `api_path` varchar(200) NOT NULL COMMENT '接口路径',
`api_method` varchar(10) DEFAULT 'GET' COMMENT '请求方法', `api_method` varchar(10) NOT NULL COMMENT '请求方法',
`api_desc` varchar(500) DEFAULT NULL COMMENT '接口描述', `sort_order` int DEFAULT 0 COMMENT '排序',
`order_num` int DEFAULT 0 COMMENT '显示顺序',
`status` char(1) DEFAULT '0' COMMENT '状态0正常 1停用', `status` char(1) DEFAULT '0' COMMENT '状态0正常 1停用',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者', `remark` varchar(500) DEFAULT NULL COMMENT '备注',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`api_id`), PRIMARY KEY (`api_id`),
UNIQUE KEY `uk_api_code` (`api_code`) UNIQUE KEY `uk_api_code` (`api_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='API接口字典表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API接口字典表';
-- API接口字典初始数据 -- ----------------------------
INSERT INTO pg_api_dict (api_code, api_name, api_path, api_method, api_desc, order_num, status) VALUES -- 应用接口授权表
('STUDENT_LIST', '查询学生信息', '/open/student/list', 'GET', '获取学生列表', 1, '0'), -- ----------------------------
('SCHOOL_LIST', '查询学校信息', '/open/school/list', 'GET', '获取学校列表', 2, '0'), DROP TABLE IF EXISTS `pg_app_api`;
('GRADE_LIST', '查询年级信息', '/open/grade/list', 'GET', '获取年级列表', 3, '0'), CREATE TABLE `pg_app_api` (
('CLASS_LIST', '查询班级信息', '/open/class/list', 'GET', '获取班级列表', 4, '0'), `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
('MEMBER_LIST', '查询会员信息', '/open/member/list', 'GET', '获取会员列表', 5, '0'), `app_id` bigint NOT NULL COMMENT '应用ID',
('REGION_TREE', '查询区域树', '/open/region/tree', 'GET', '获取区域树形结构', 6, '0'); `api_id` bigint NOT NULL COMMENT '接口ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_app_api` (`app_id`, `api_id`),
KEY `idx_app_id` (`app_id`),
KEY `idx_api_id` (`api_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用接口授权表';
-- ----------------------------
-- 初始化API接口字典数据
-- ----------------------------
INSERT INTO pg_api_dict (api_code, api_name, api_path, api_method, sort_order, status, create_time) VALUES
('api_member_info', '获取会员信息', '/open/api/member/info', 'GET', 1, '0', NOW()),
('api_member_list', '获取会员列表', '/open/api/member/list', 'GET', 2, '0', NOW()),
('api_student_info', '获取学生信息', '/open/api/student/info', 'GET', 3, '0', NOW()),
('api_student_list', '获取学生列表', '/open/api/student/list', 'GET', 4, '0', NOW()),
('api_school_info', '获取学校信息', '/open/api/school/info', 'GET', 5, '0', NOW()),
('api_school_list', '获取学校列表', '/open/api/school/list', 'GET', 6, '0', NOW());
-- ----------------------------
-- 初始化应用示例数据
-- ----------------------------
INSERT INTO pg_application (app_code, app_name, app_secret, contact_person, contact_phone, status, create_by, create_time, del_flag) VALUES
('YY000001', 'AI智慧平台', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', '张三', '13800138000', '0', 'admin', NOW(), '0'),
('YY000002', '测试应用', 'p6o5n4m3l2k1j0i9h8g7f6e5d4c3b2a1', '李四', '13900139000', '0', 'admin', NOW(), '0');
-- ----------------------------
-- 初始化应用接口授权数据
-- ----------------------------
INSERT INTO pg_app_api (app_id, api_id, create_time) VALUES
(1, 1, NOW()),
(1, 2, NOW()),
(1, 3, NOW()),
(1, 4, NOW()),
(2, 1, NOW()),
(2, 3, NOW());