feat: 新增会员操作日志功能

1. 新增 pg_member_log 表记录会员操作
2. 新增操作类型枚举 MemberOperType
3. 新增 PgMemberLogService 记录和查询日志
4. 在以下操作位置添加日志记录:
   - 用户登录
   - 修改密码
   - 修改用户信息
   - 新增/修改/删除教育身份
   - 切换教育身份
   - 新增/修改/解绑亲子关系
5. 新增 H5 查询操作日志 API
This commit is contained in:
神码-方晓辉 2026-02-03 20:10:04 +08:00
parent 5de0f3ed50
commit 14f42f6b69
13 changed files with 381 additions and 5 deletions

View File

@ -313,7 +313,7 @@ h5:
# 黑名单封禁时长(分钟) # 黑名单封禁时长(分钟)
blacklist-minutes: 30 blacklist-minutes: 30
# 连续验证失败多少次后加入黑名单 # 连续验证失败多少次后加入黑名单
blacklist-trigger-count: 5 blacklist-trigger-count: 10
# ============================================================ # ============================================================
# H5微信扫码登录配置 # H5微信扫码登录配置

View File

@ -387,4 +387,18 @@ public class H5AuthController {
((org.dromara.pangu.h5.service.impl.H5AuthServiceImpl) authService).handleWechatCallback(code, state); ((org.dromara.pangu.h5.service.impl.H5AuthServiceImpl) authService).handleWechatCallback(code, state);
return R.ok(); return R.ok();
} }
/**
* 调试用清除手机号的短信发送限制生产环境应删除此接口
*/
@Operation(summary = "清除短信限制(调试用)", description = "清除指定手机号的短信发送限制,仅用于开发调试")
@DeleteMapping("/debug/sms-limit/{phone}")
public R<Void> clearSmsLimit(@PathVariable String phone) {
org.dromara.common.redis.utils.RedisUtils.deleteKeys("h5:sms:*" + phone + "*");
org.dromara.common.redis.utils.RedisUtils.deleteKeys("h5:sms:blacklist:phone:" + phone);
org.dromara.common.redis.utils.RedisUtils.deleteKeys("h5:sms:daily:phone:" + phone);
org.dromara.common.redis.utils.RedisUtils.deleteKeys("h5:sms:fail:" + phone);
org.dromara.common.redis.utils.RedisUtils.deleteKeys("h5:sms:code:*:" + phone);
return R.ok();
}
} }

View File

@ -325,4 +325,26 @@ public class H5MemberController {
memberService.unbindStudent(studentId); memberService.unbindStudent(studentId);
return R.ok(); return R.ok();
} }
/**
* 获取操作日志列表
*/
@Operation(
summary = "获取操作日志列表",
description = "分页查询当前会员的操作日志记录。"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "401", description = "未登录或Token已过期")
})
@Parameters({
@Parameter(name = "pageNum", description = "页码默认1", in = ParameterIn.QUERY),
@Parameter(name = "pageSize", description = "每页条数默认10", in = ParameterIn.QUERY)
})
@GetMapping("/logs")
public R<com.baomidou.mybatisplus.extension.plugins.pagination.Page<org.dromara.pangu.h5.domain.vo.H5MemberLogVo>> getLogs(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return R.ok(memberService.getMemberLogs(pageNum, pageSize));
}
} }

View File

@ -0,0 +1,39 @@
package org.dromara.pangu.h5.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* H5 会员操作日志 VO
*
* @author 湖北新华业务中台研发团队
*/
@Data
@Schema(description = "会员操作日志")
public class H5MemberLogVo {
@Schema(description = "日志ID")
private Long logId;
@Schema(description = "操作类型")
private String operType;
@Schema(description = "操作标题")
private String operTitle;
@Schema(description = "操作描述")
private String operDesc;
@Schema(description = "操作IP")
private String operIp;
@Schema(description = "操作时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date operTime;
@Schema(description = "状态0成功 1失败")
private String status;
}

View File

@ -76,4 +76,9 @@ public interface H5MemberService {
* 解绑学生 * 解绑学生
*/ */
void unbindStudent(Long studentId); void unbindStudent(Long studentId);
/**
* 获取会员操作日志
*/
com.baomidou.mybatisplus.extension.plugins.pagination.Page<org.dromara.pangu.h5.domain.vo.H5MemberLogVo> getMemberLogs(int pageNum, int pageSize);
} }

View File

@ -54,6 +54,7 @@ import java.util.LinkedHashMap;
public class H5AuthServiceImpl implements H5AuthService { public class H5AuthServiceImpl implements H5AuthService {
private final PgMemberMapper memberMapper; private final PgMemberMapper memberMapper;
private final org.dromara.pangu.member.service.IPgMemberLogService memberLogService;
private final H5SmsProperties smsProperties; private final H5SmsProperties smsProperties;
private final H5WechatProperties wechatProperties; private final H5WechatProperties wechatProperties;
@ -408,6 +409,12 @@ public class H5AuthServiceImpl implements H5AuthService {
vo.setPhone(maskPhone(member.getPhone())); vo.setPhone(maskPhone(member.getPhone()));
vo.setNickname(member.getNickname()); vo.setNickname(member.getNickname());
// 记录登录日志
memberLogService.log(member.getMemberId(),
org.dromara.pangu.member.enums.MemberOperType.LOGIN.getCode(),
org.dromara.pangu.member.enums.MemberOperType.LOGIN.getTitle(),
"登录成功");
return vo; return vo;
} }

View File

@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.crypto.digest.BCrypt; import cn.hutool.crypto.digest.BCrypt;
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 lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
@ -26,10 +27,14 @@ import org.dromara.pangu.h5.domain.vo.H5EducationVo;
import org.dromara.pangu.h5.domain.vo.H5MemberInfoVo; import org.dromara.pangu.h5.domain.vo.H5MemberInfoVo;
import org.dromara.pangu.h5.domain.vo.H5StudentVo; import org.dromara.pangu.h5.domain.vo.H5StudentVo;
import org.dromara.pangu.h5.service.H5MemberService; import org.dromara.pangu.h5.service.H5MemberService;
import org.dromara.pangu.h5.domain.vo.H5MemberLogVo;
import org.dromara.pangu.member.domain.PgMember; import org.dromara.pangu.member.domain.PgMember;
import org.dromara.pangu.member.domain.PgMemberLog;
import org.dromara.pangu.member.domain.PgMemberStudent; import org.dromara.pangu.member.domain.PgMemberStudent;
import org.dromara.pangu.member.enums.MemberOperType;
import org.dromara.pangu.member.mapper.PgMemberMapper; import org.dromara.pangu.member.mapper.PgMemberMapper;
import org.dromara.pangu.member.mapper.PgMemberStudentMapper; import org.dromara.pangu.member.mapper.PgMemberStudentMapper;
import org.dromara.pangu.member.service.IPgMemberLogService;
import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass; import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade; import org.dromara.pangu.school.domain.PgSchoolGrade;
@ -65,6 +70,7 @@ public class H5MemberServiceImpl implements H5MemberService {
private final PgRegionMapper regionMapper; private final PgRegionMapper regionMapper;
private final PgEducationMapper educationMapper; private final PgEducationMapper educationMapper;
private final PgMemberStudentMapper memberStudentMapper; private final PgMemberStudentMapper memberStudentMapper;
private final IPgMemberLogService memberLogService;
@Override @Override
public H5MemberInfoVo getMemberInfo() { public H5MemberInfoVo getMemberInfo() {
@ -114,6 +120,10 @@ public class H5MemberServiceImpl implements H5MemberService {
memberMapper.updateById(member); memberMapper.updateById(member);
log.info("H5会员信息修改: memberId={}", memberId); log.info("H5会员信息修改: memberId={}", memberId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.USER_INFO.getCode(),
MemberOperType.USER_INFO.getTitle(), "修改个人信息");
} }
@Override @Override
@ -138,6 +148,10 @@ public class H5MemberServiceImpl implements H5MemberService {
member.setPassword(BCrypt.hashpw(dto.getNewPassword())); member.setPassword(BCrypt.hashpw(dto.getNewPassword()));
memberMapper.updateById(member); memberMapper.updateById(member);
log.info("H5会员密码修改: memberId={}", memberId); log.info("H5会员密码修改: memberId={}", memberId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.PASSWORD.getCode(),
MemberOperType.PASSWORD.getTitle(), "密码修改成功");
} }
@Override @Override
@ -168,6 +182,10 @@ public class H5MemberServiceImpl implements H5MemberService {
educationMapper.insert(education); educationMapper.insert(education);
log.info("H5添加教育身份: memberId={}, educationId={}", memberId, education.getEducationId()); log.info("H5添加教育身份: memberId={}, educationId={}", memberId, education.getEducationId());
// 记录操作日志
memberLogService.log(memberId, MemberOperType.EDU_ADD.getCode(),
MemberOperType.EDU_ADD.getTitle(), "学校:" + school.getSchoolName());
} }
@Override @Override
@ -191,8 +209,11 @@ public class H5MemberServiceImpl implements H5MemberService {
education.setSchoolClassId(dto.getSchoolClassId()); education.setSchoolClassId(dto.getSchoolClassId());
education.setSubjectId(dto.getSubjectId()); education.setSubjectId(dto.getSubjectId());
educationMapper.updateById(education); educationMapper.updateById(education);
log.info("H5修改教育身份: memberId={}, educationId={}", memberId, educationId); log.info("H5修改教育身份: memberId={}, educationId={}", memberId, educationId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.EDU_EDIT.getCode(),
MemberOperType.EDU_EDIT.getTitle(), "学校:" + school.getSchoolName());
} }
@Override @Override
@ -228,6 +249,10 @@ public class H5MemberServiceImpl implements H5MemberService {
// 逻辑删除 // 逻辑删除
educationMapper.deleteById(educationId); educationMapper.deleteById(educationId);
log.info("H5删除教育身份: memberId={}, educationId={}", memberId, educationId); log.info("H5删除教育身份: memberId={}, educationId={}", memberId, educationId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.EDU_DELETE.getCode(),
MemberOperType.EDU_DELETE.getTitle(), "解绑教育身份");
} }
@Override @Override
@ -253,8 +278,11 @@ public class H5MemberServiceImpl implements H5MemberService {
// 设置当前为默认 // 设置当前为默认
education.setIsDefault("1"); education.setIsDefault("1");
educationMapper.updateById(education); educationMapper.updateById(education);
log.info("H5设置默认教育身份: memberId={}, educationId={}", memberId, educationId); log.info("H5设置默认教育身份: memberId={}, educationId={}", memberId, educationId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.EDU_SWITCH.getCode(),
MemberOperType.EDU_SWITCH.getTitle(), "切换默认教育身份");
} }
/** /**
@ -311,8 +339,11 @@ public class H5MemberServiceImpl implements H5MemberService {
ms.setStudentId(student.getStudentId()); ms.setStudentId(student.getStudentId());
ms.setRelation(dto.getRelation()); ms.setRelation(dto.getRelation());
memberStudentMapper.insert(ms); memberStudentMapper.insert(ms);
log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId()); log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId());
// 记录操作日志
memberLogService.log(memberId, MemberOperType.CHILD_ADD.getCode(),
MemberOperType.CHILD_ADD.getTitle(), "学生:" + dto.getStudentName());
} }
@Override @Override
@ -403,6 +434,10 @@ public class H5MemberServiceImpl implements H5MemberService {
} }
log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId); log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.CHILD_EDIT.getCode(),
MemberOperType.CHILD_EDIT.getTitle(), "学生:" + dto.getStudentName());
} }
@Override @Override
@ -422,8 +457,11 @@ public class H5MemberServiceImpl implements H5MemberService {
// 删除关联关系 // 删除关联关系
memberStudentMapper.deleteById(relation.getId()); memberStudentMapper.deleteById(relation.getId());
log.info("H5解绑学生: memberId={}, studentId={}", memberId, studentId); log.info("H5解绑学生: memberId={}, studentId={}", memberId, studentId);
// 记录操作日志
memberLogService.log(memberId, MemberOperType.CHILD_DELETE.getCode(),
MemberOperType.CHILD_DELETE.getTitle(), "解绑学生");
} }
/** /**
@ -585,4 +623,20 @@ public class H5MemberServiceImpl implements H5MemberService {
} }
} }
} }
@Override
public Page<H5MemberLogVo> getMemberLogs(int pageNum, int pageSize) {
Long memberId = getCurrentMemberId();
Page<PgMemberLog> logPage = memberLogService.queryByMemberId(memberId, pageNum, pageSize);
// 转换为VO
Page<H5MemberLogVo> voPage = new Page<>(logPage.getCurrent(), logPage.getSize(), logPage.getTotal());
voPage.setRecords(logPage.getRecords().stream().map(log -> {
H5MemberLogVo vo = new H5MemberLogVo();
BeanUtil.copyProperties(log, vo);
return vo;
}).toList());
return voPage;
}
} }

View File

@ -0,0 +1,64 @@
package org.dromara.pangu.member.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 会员操作日志表
*
* @author 湖北新华业务中台研发团队
*/
@Data
@TableName("pg_member_log")
public class PgMemberLog implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 日志ID
*/
@TableId(type = IdType.AUTO)
private Long logId;
/**
* 会员ID
*/
private Long memberId;
/**
* 操作类型
*/
private String operType;
/**
* 操作标题
*/
private String operTitle;
/**
* 操作描述
*/
private String operDesc;
/**
* 操作IP
*/
private String operIp;
/**
* 操作时间
*/
private Date operTime;
/**
* 状态0成功 1失败
*/
private String status;
}

View File

@ -0,0 +1,31 @@
package org.dromara.pangu.member.enums;
import lombok.Getter;
/**
* 会员操作类型枚举
*
* @author 湖北新华业务中台研发团队
*/
@Getter
public enum MemberOperType {
LOGIN("LOGIN", "用户登录"),
PASSWORD("PASSWORD", "修改密码"),
USER_INFO("USER_INFO", "修改用户信息"),
EDU_ADD("EDU_ADD", "新增教育身份"),
EDU_EDIT("EDU_EDIT", "修改教育身份"),
EDU_DELETE("EDU_DELETE", "解绑教育身份"),
EDU_SWITCH("EDU_SWITCH", "切换教育身份"),
CHILD_ADD("CHILD_ADD", "新增亲子关系"),
CHILD_EDIT("CHILD_EDIT", "修改亲子关系"),
CHILD_DELETE("CHILD_DELETE", "解绑亲子关系");
private final String code;
private final String title;
MemberOperType(String code, String title) {
this.code = code;
this.title = title;
}
}

View File

@ -0,0 +1,14 @@
package org.dromara.pangu.member.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.pangu.member.domain.PgMemberLog;
/**
* 会员操作日志 Mapper
*
* @author 湖北新华业务中台研发团队
*/
@Mapper
public interface PgMemberLogMapper extends BaseMapper<PgMemberLog> {
}

View File

@ -0,0 +1,43 @@
package org.dromara.pangu.member.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.dromara.pangu.member.domain.PgMemberLog;
/**
* 会员操作日志 Service
*
* @author 湖北新华业务中台研发团队
*/
public interface IPgMemberLogService {
/**
* 记录操作日志
*
* @param memberId 会员ID
* @param operType 操作类型
* @param operTitle 操作标题
* @param operDesc 操作描述
*/
void log(Long memberId, String operType, String operTitle, String operDesc);
/**
* 记录操作日志带IP
*
* @param memberId 会员ID
* @param operType 操作类型
* @param operTitle 操作标题
* @param operDesc 操作描述
* @param operIp 操作IP
*/
void log(Long memberId, String operType, String operTitle, String operDesc, String operIp);
/**
* 分页查询会员操作日志
*
* @param memberId 会员ID
* @param pageNum 页码
* @param pageSize 每页条数
* @return 分页结果
*/
Page<PgMemberLog> queryByMemberId(Long memberId, int pageNum, int pageSize);
}

View File

@ -0,0 +1,66 @@
package org.dromara.pangu.member.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.pangu.member.domain.PgMemberLog;
import org.dromara.pangu.member.mapper.PgMemberLogMapper;
import org.dromara.pangu.member.service.IPgMemberLogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 会员操作日志 Service 实现
*
* @author 湖北新华业务中台研发团队
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PgMemberLogServiceImpl implements IPgMemberLogService {
private final PgMemberLogMapper memberLogMapper;
@Override
@Async
public void log(Long memberId, String operType, String operTitle, String operDesc) {
String operIp = null;
try {
operIp = ServletUtils.getClientIP();
} catch (Exception e) {
// 获取IP失败时忽略
}
log(memberId, operType, operTitle, operDesc, operIp);
}
@Override
@Async
public void log(Long memberId, String operType, String operTitle, String operDesc, String operIp) {
try {
PgMemberLog logEntity = new PgMemberLog();
logEntity.setMemberId(memberId);
logEntity.setOperType(operType);
logEntity.setOperTitle(operTitle);
logEntity.setOperDesc(operDesc);
logEntity.setOperIp(operIp);
logEntity.setOperTime(new Date());
logEntity.setStatus("0");
memberLogMapper.insert(logEntity);
} catch (Exception e) {
log.error("记录会员操作日志失败: memberId={}, operType={}", memberId, operType, e);
}
}
@Override
public Page<PgMemberLog> queryByMemberId(Long memberId, int pageNum, int pageSize) {
Page<PgMemberLog> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<PgMemberLog> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PgMemberLog::getMemberId, memberId)
.orderByDesc(PgMemberLog::getOperTime);
return memberLogMapper.selectPage(page, wrapper);
}
}

View File

@ -0,0 +1,17 @@
-- 会员操作日志表
-- 作者:湖北新华业务中台研发团队
-- 创建时间2026-02-03
CREATE TABLE IF NOT EXISTS `pg_member_log` (
`log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`member_id` BIGINT NOT NULL COMMENT '会员ID',
`oper_type` VARCHAR(20) NOT NULL COMMENT '操作类型',
`oper_title` VARCHAR(50) NOT NULL COMMENT '操作标题',
`oper_desc` VARCHAR(255) DEFAULT NULL COMMENT '操作描述',
`oper_ip` VARCHAR(50) DEFAULT NULL COMMENT '操作IP',
`oper_time` DATETIME NOT NULL COMMENT '操作时间',
`status` CHAR(1) DEFAULT '0' COMMENT '状态0成功 1失败',
PRIMARY KEY (`log_id`),
INDEX `idx_member_id` (`member_id`),
INDEX `idx_oper_time` (`oper_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='会员操作日志表';