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

View File

@ -3,51 +3,57 @@ package com.pangu.application.domain.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.List;
/**
* 应用传输对象
* 应用数据传输对象
*
* @author pangu
*/
@Data
public class ApplicationDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 应用ID(编辑时必填) */
/** 应用ID */
private Long appId;
/** 应用编码 */
private String appCode;
/** 应用名称 */
@NotBlank(message = "应用名称不能为空")
@Size(max = 100, message = "应用名称不能超过100个字符")
private String appName;
/** 应用编码(查询条件) */
private String appCode;
/** 应用描述 */
@Size(max = 500, message = "应用描述不能超过500个字符")
private String appDesc;
/** 应用密钥 */
private String appSecret;
/** 联系人 */
@Size(max = 50, message = "联系人不能超过50个字符")
private String contactPerson;
/** 联系电话 */
@Size(max = 20, message = "联系电话不能超过20个字符")
private String contactPhone;
/** 状态0正常 1停用 */
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 pageSize;
}

View File

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

View File

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

View File

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

View File

@ -4,41 +4,70 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.time.LocalDateTime;
import java.util.List;
/**
* 应用视图对象
*
* @author pangu
*/
@Data
public class ApplicationVO implements Serializable {
private static final long serialVersionUID = 1L;
/** 应用ID */
private Long appId;
/** 应用编码 */
private String appCode;
/** 应用名称 */
private String appName;
private String appDesc;
/** 应用密钥 */
private String appSecret;
/** 联系人 */
private String contactPerson;
/** 联系电话 */
private String contactPhone;
/** 状态0正常 1停用 */
private String status;
private String createBy;
/** 创建时间 */
@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
public static class ApiInfo implements Serializable {
public static class ApiVO implements Serializable {
private static final long serialVersionUID = 1L;
/** 接口ID */
private Long apiId;
/** 接口编码 */
private String apiCode;
/** 接口名称 */
private String apiName;
/** 接口路径 */
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.pangu.application.domain.entity.ApiDict;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* API接口字典Mapper接口
*
* @author pangu
*/
@Mapper
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.pangu.application.domain.entity.AppApi;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 应用接口授权Mapper接口
*
* @author pangu
*/
@Mapper
public interface AppApiMapper extends BaseMapper<AppApi> {
/**
* 根据应用ID查询授权接口
* 批量插入应用接口授权
*
* @param list 授权列表
* @return 影响行数
*/
@Select("SELECT * FROM pg_app_grant WHERE app_id = #{appId}")
List<AppApi> selectByAppId(@Param("appId") Long appId);
int batchInsert(@Param("list") List<AppApi> list);
/**
* 根据应用ID删除授权接口
* 根据应用ID删除授权
*
* @param appId 应用ID
* @return 影响行数
*/
@Delete("DELETE FROM pg_app_grant WHERE app_id = #{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;
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.vo.ApplicationVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 应用Mapper接口
*
* @author pangu
*/
@Mapper
public interface ApplicationMapper extends BaseMapper<Application> {
/**
* 查询最大应用编码
* 查询应用列表
*
* @param page 分页对象
* @param dto 查询条件
* @return 应用列表
*/
@Select("SELECT MAX(app_code) FROM pg_application WHERE del_flag = '0'")
String selectMaxAppCode();
List<ApplicationVO> selectApplicationVOList(Page<ApplicationVO> page, @Param("dto") ApplicationDTO dto);
/**
* 根据应用编码查询应用
* 根据ID查询应用详情
*
* @param appId 应用ID
* @return 应用详情
*/
@Select("SELECT * FROM pg_application WHERE app_code = #{appCode} AND del_flag = '0'")
Application selectByAppCode(@Param("appCode") String appCode);
ApplicationVO selectApplicationVOById(@Param("appId") Long appId);
/**
* 检查应用名称是否存在排除指定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 checkAppNameExists(@Param("appName") String appName, @Param("excludeId") Long excludeId);
int countByAppCode(@Param("appCode") String appCode, @Param("appId") Long appId);
/**
* 获取下一个应用编码序号
*
* @return 序号
*/
int getNextCodeSeq();
}

View File

@ -1,63 +1,82 @@
package com.pangu.application.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pangu.application.domain.dto.ApplicationDTO;
import com.pangu.application.domain.entity.ApiDict;
import com.pangu.application.domain.entity.Application;
import com.pangu.application.domain.vo.ApplicationVO;
import com.pangu.common.core.page.TableDataInfo;
import java.util.List;
import java.util.Map;
/**
* 应用管理服务接口
* 应用服务接口
*
* @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 新密钥
*/
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 checkApiPermission(String appCode, String apiPath);
boolean checkAppCodeUnique(String appCode, Long appId);
}

View File

@ -1,8 +1,10 @@
package com.pangu.application.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
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.entity.ApiDict;
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.service.IApplicationService;
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.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 应用管理服务实现
* 应用服务实现
*
* @author pangu
*/
@Slf4j
@Service
public class ApplicationServiceImpl implements IApplicationService {
@RequiredArgsConstructor
public class ApplicationServiceImpl extends ServiceImpl<ApplicationMapper, Application> implements IApplicationService {
@Resource
private ApplicationMapper applicationMapper;
@Resource
private AppApiMapper appApiMapper;
@Resource
private ApiDictMapper apiDictMapper;
private final ApplicationMapper applicationMapper;
private final AppApiMapper appApiMapper;
private final ApiDictMapper apiDictMapper;
@Override
public List<ApplicationVO> selectApplicationList(ApplicationDTO dto) {
LambdaQueryWrapper<Application> wrapper = new LambdaQueryWrapper<>();
if (dto != null && dto.getAppName() != null && !dto.getAppName().isEmpty()) {
wrapper.like(Application::getAppName, dto.getAppName());
}
if (dto != null && dto.getAppCode() != null && !dto.getAppCode().isEmpty()) {
wrapper.like(Application::getAppCode, dto.getAppCode());
}
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;
public TableDataInfo selectApplicationList(ApplicationDTO applicationDTO) {
Page<ApplicationVO> page = new Page<>(
applicationDTO.getPageNum() != null ? applicationDTO.getPageNum() : 1,
applicationDTO.getPageSize() != null ? applicationDTO.getPageSize() : 10
);
List<ApplicationVO> list = applicationMapper.selectApplicationVOList(page, applicationDTO);
return new TableDataInfo(list, page.getTotal());
}
@Override
public ApplicationVO selectApplicationById(Long appId) {
Application app = applicationMapper.selectById(appId);
if (app == null) {
throw new ServiceException("应用不存在");
}
return toVO(app);
public ApplicationVO getApplicationById(Long appId) {
return applicationMapper.selectApplicationVOById(appId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> insertApplication(ApplicationDTO dto) {
if (applicationMapper.checkAppNameExists(dto.getAppName(), null) > 0) {
throw new ServiceException("应用名称已存在");
}
public ApplicationVO insertApplication(ApplicationDTO applicationDTO) {
// 生成应用编码
String appCode = generateAppCode();
// 生成应用密钥
String appSecret = generateAppSecret();
Application app = new Application();
app.setAppCode(appCode);
app.setAppName(dto.getAppName());
app.setAppSecret(appSecret);
app.setAppDesc(dto.getAppDesc());
app.setContactPerson(dto.getContactPerson());
app.setContactPhone(dto.getContactPhone());
app.setStatus(dto.getStatus() != null ? dto.getStatus() : "0");
app.setCreateBy("admin");
app.setCreateTime(DateUtils.getNowDate());
app.setDelFlag("0");
applicationMapper.insert(app);
saveAppApis(app.getAppId(), dto.getApiCodes());
Map<String, String> result = new HashMap<>();
result.put("appCode", appCode);
result.put("appSecret", appSecret);
// 构建应用对象
Application application = buildApplication(applicationDTO);
application.setAppCode(appCode);
application.setAppSecret(appSecret);
application.setStatus("0");
// 保存应用
int result = applicationMapper.insert(application);
if (result <= 0) {
throw new ServiceException("新增应用失败");
}
// 保存接口授权
saveAppApis(application.getAppId(), applicationDTO.getApiIds());
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;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateApplication(ApplicationDTO dto) {
if (dto.getAppId() == null) {
throw new ServiceException("应用ID不能为空");
}
Application existApp = applicationMapper.selectById(dto.getAppId());
if (existApp == null) {
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);
public int deleteApplication(Long appId) {
// 软删除应用
Application application = new Application();
application.setAppId(appId);
application.setDelFlag("1");
int result = applicationMapper.updateById(application);
// 删除接口授权
appApiMapper.deleteByAppId(appId);
return rows;
log.info("删除应用成功, appId={}", appId);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String resetAppSecret(Long appId) {
Application app = applicationMapper.selectById(appId);
if (app == null) {
throw new ServiceException("应用不存在");
}
public String resetSecret(Long appId) {
// 生成新密钥
String newSecret = generateAppSecret();
Application updateApp = new Application();
updateApp.setAppId(appId);
updateApp.setAppSecret(newSecret);
updateApp.setUpdateBy("admin");
updateApp.setUpdateTime(DateUtils.getNowDate());
applicationMapper.updateById(updateApp);
// 更新密钥
Application application = new Application();
application.setAppId(appId);
application.setAppSecret(newSecret);
int result = applicationMapper.updateById(application);
if (result <= 0) {
throw new ServiceException("重置密钥失败");
}
log.info("重置应用密钥成功, appId={}", appId);
return newSecret;
}
@Override
public List<ApiDict> selectApiDictList() {
return apiDictMapper.selectEnabledList();
public List<ApiDict> getApiList() {
LambdaQueryWrapper<ApiDict> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ApiDict::getStatus, "0");
wrapper.orderByAsc(ApiDict::getSortOrder);
return apiDictMapper.selectList(wrapper);
}
@Override
public Application selectByAppCode(String appCode) {
return applicationMapper.selectByAppCode(appCode);
}
@Override
public boolean checkApiPermission(String appCode, String apiPath) {
List<String> paths = appApiMapper.selectApiPathsByAppCode(appCode);
return paths != null && paths.contains(apiPath);
public boolean checkAppCodeUnique(String appCode, Long appId) {
int count = applicationMapper.countByAppCode(appCode, appId);
return count == 0;
}
/**
* 生成应用编码
* 格式YY + 6位序号
*/
private String generateAppCode() {
String maxCode = applicationMapper.selectMaxAppCode();
int nextSeq = 1;
if (maxCode != null && maxCode.length() > 2) {
try {
nextSeq = Integer.parseInt(maxCode.substring(2)) + 1;
} catch (NumberFormatException ignored) {
}
}
return String.format("YY%06d", nextSeq);
int seq = applicationMapper.getNextCodeSeq();
return String.format("YY%06d", seq);
}
/**
* 生成应用密钥
* 32位随机字符串
*/
private String generateAppSecret() {
return RandomUtil.randomString(32);
}
private void saveAppApis(Long appId, List<String> apiCodes) {
if (CollUtil.isEmpty(apiCodes)) {
return;
}
List<ApiDict> apiDicts = apiDictMapper.selectByCodes(apiCodes);
Map<String, ApiDict> dictMap = apiDicts.stream().collect(Collectors.toMap(ApiDict::getApiCode, d -> d));
String username = "admin";
java.util.Date now = DateUtils.getNowDate();
for (String apiCode : apiCodes) {
ApiDict dict = dictMap.get(apiCode);
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 Application buildApplication(ApplicationDTO dto) {
Application application = new Application();
application.setAppName(dto.getAppName());
application.setContactPerson(dto.getContactPerson());
application.setContactPhone(dto.getContactPhone());
application.setRemark(dto.getRemark());
return application;
}
private ApplicationVO toVO(Application app) {
ApplicationVO vo = new ApplicationVO();
vo.setAppId(app.getAppId());
vo.setAppCode(app.getAppCode());
vo.setAppName(app.getAppName());
vo.setAppDesc(app.getAppDesc());
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);
/**
* 保存接口授权
*/
private void saveAppApis(Long appId, List<Long> apiIds) {
if (apiIds == null || apiIds.isEmpty()) {
return;
}
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);
/**
* 根据区域路径查询区域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);
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 会员信息
*/
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 BCryptPasswordEncoder passwordEncoder;
private final com.pangu.student.service.IStudentService studentService;
/** 默认密码 */
private static final String DEFAULT_PASSWORD = "123456";
@ -68,8 +69,9 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
// 填充名称
fillMemberVONames(memberVO);
// 查询绑定的学生暂时返回空列表等学生模块完成后再实现
memberVO.setStudents(new ArrayList<>());
// 查询绑定的学生
List<com.pangu.student.domain.vo.StudentVO> students = studentService.selectStudentVOsByMemberId(memberId);
memberVO.setStudents(students);
return memberVO;
}
@ -194,24 +196,26 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
throw new ServiceException("会员不存在");
}
// TODO: 教师只能绑定本校学生等学生模块完成后实现
// if (IdentityTypeEnum.isTeacher(member.getIdentityType())) {
// if (!studentService.isStudentInSchool(studentId, member.getSchoolId())) {
// throw new ServiceException("教师只能绑定本校学生");
// }
// }
// 教师只能绑定本校学生
if (IdentityTypeEnum.isTeacher(member.getIdentityType())) {
if (!studentService.isStudentInSchool(studentId, member.getSchoolId())) {
throw new ServiceException("教师只能绑定本校学生");
}
}
// TODO: 调用学生模块的接口更新学生的memberId等学生模块完成后实现
// 更新学生的memberId
int result = studentService.updateStudentMember(studentId, memberId);
log.info("绑定学生成功, memberId={}, studentId={}", memberId, studentId);
return 1;
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int unbindStudent(Long memberId, Long studentId) {
// TODO: 调用学生模块的接口解绑等学生模块完成后实现
// 解绑学生
int result = studentService.unbindStudent(studentId);
log.info("解绑学生成功, memberId={}, studentId={}", memberId, studentId);
return 1;
return result;
}
@Override
@ -221,9 +225,8 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
@Override
public boolean checkCanDelete(Long memberId) {
// TODO: 检查是否有绑定的学生等学生模块完成后实现
// return studentService.countByMemberId(memberId) == 0;
return true;
// 检查是否有绑定的学生
return studentService.countByMemberId(memberId) == 0;
}
@Override
@ -312,4 +315,30 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> impleme
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 影响行数
*/
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;
}).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;
import com.alibaba.excel.EasyExcel;
import com.pangu.common.core.controller.BaseController;
import com.pangu.common.core.domain.AjaxResult;
import com.pangu.common.core.page.TableDataInfo;
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.StudentVO;
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.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
*
@ -78,8 +87,29 @@ public class StudentController extends BaseController {
* 下载导入模板
*/
@GetMapping("/template")
public void downloadTemplate() {
// TODO: 实现模板下载
public void downloadTemplate(HttpServletResponse response) throws IOException {
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 com.alibaba.excel.context.AnalysisContext;
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.vo.ImportResultVO;
import com.pangu.student.service.IStudentService;
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> {
private final IStudentService studentService;
private final IRegionService regionService;
private final ISchoolService schoolService;
private final IMemberService memberService;
private final ImportResultVO result = new ImportResultVO();
private int rowNum = 1; // 从第2行开始第1行是表头
public StudentImportListener(IStudentService studentService) {
public StudentImportListener(IStudentService studentService,
IRegionService regionService,
ISchoolService schoolService,
IMemberService memberService) {
this.studentService = studentService;
this.regionService = regionService;
this.schoolService = schoolService;
this.memberService = memberService;
result.setTotal(0);
result.setSuccessCount(0);
result.setFailCount(0);
@ -41,15 +57,60 @@ public class StudentImportListener extends AnalysisEventListener<StudentImportDT
return;
}
// TODO: 实际导入逻辑需要查询区域学校会员等信息
// 这里简化处理实际需要
// 1. 根据区域路径查询区域ID
// 2. 根据学校名称查询学校ID
// 3. 根据年级名称查询学校年级ID
// 4. 根据班级名称查询学校班级ID
// 5. 根据手机号查询或创建会员
// 6. 保存学生信息
Long regionId = regionService.getRegionIdByPath(data.getRegionPath());
if (regionId == null) {
addError(rowNum, data, "区域不存在:" + data.getRegionPath());
result.setFailCount(result.getFailCount() + 1);
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);
log.info("导入学生成功:{}", data.getStudentName());
@ -109,4 +170,34 @@ public class StudentImportListener extends AnalysisEventListener<StudentImportDT
public ImportResultVO getResult() {
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 数量
*/
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不唯一
*/
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 {
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
public TableDataInfo selectStudentList(StudentDTO studentDTO) {
@ -99,7 +102,7 @@ public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> impl
@Transactional(rollbackFor = Exception.class)
public ImportResultVO importStudents(MultipartFile file) {
try {
StudentImportListener listener = new StudentImportListener(this);
StudentImportListener listener = new StudentImportListener(this, regionService, schoolService, memberService);
EasyExcel.read(file.getInputStream(), StudentImportDTO.class, listener).sheet().doRead();
return listener.getResult();
} catch (IOException e) {
@ -114,6 +117,47 @@ public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> impl
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>
</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>

View File

@ -1,16 +1,18 @@
-- ============================================================
-- 盘古用户平台 - 应用管理模块表结构及初始数据
-- 作者:湖北新华业务中台研发团队
-- 说明:执行前请确认已存在 sys_menu菜单已在 pangu_menu.sql 中
-- 应用管理模块 - 数据库脚本
-- 作者:pangu
-- 创建时间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_code` varchar(32) NOT NULL COMMENT '应用编码',
`app_name` varchar(100) 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_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`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删除',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`app_id`),
UNIQUE KEY `uk_app_code` (`app_code`),
UNIQUE KEY `uk_app_name` (`app_name`, `del_flag`)
) 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='应用接口授权表';
UNIQUE KEY `uk_app_code` (`app_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用表';
-- ----------------------------
-- 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_code` varchar(100) NOT NULL COMMENT '接口编码',
`api_code` varchar(50) NOT NULL COMMENT '接口编码',
`api_name` varchar(100) NOT NULL COMMENT '接口名称',
`api_path` varchar(200) NOT NULL COMMENT '接口路径',
`api_method` varchar(10) DEFAULT 'GET' COMMENT '请求方法',
`api_desc` varchar(500) DEFAULT NULL COMMENT '接口描述',
`order_num` int DEFAULT 0 COMMENT '显示顺序',
`api_method` varchar(10) NOT NULL COMMENT '请求方法',
`sort_order` int DEFAULT 0 COMMENT '排序',
`status` char(1) DEFAULT '0' COMMENT '状态0正常 1停用',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`api_id`),
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'),
('GRADE_LIST', '查询年级信息', '/open/grade/list', 'GET', '获取年级列表', 3, '0'),
('CLASS_LIST', '查询班级信息', '/open/class/list', 'GET', '获取班级列表', 4, '0'),
('MEMBER_LIST', '查询会员信息', '/open/member/list', 'GET', '获取会员列表', 5, '0'),
('REGION_TREE', '查询区域树', '/open/region/tree', 'GET', '获取区域树形结构', 6, '0');
-- ----------------------------
-- 应用接口授权表
-- ----------------------------
DROP TABLE IF EXISTS `pg_app_api`;
CREATE TABLE `pg_app_api` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`app_id` bigint NOT NULL COMMENT '应用ID',
`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());