diff --git a/backend/ruoyi-admin/src/main/resources/application.yml b/backend/ruoyi-admin/src/main/resources/application.yml index 7128404..d789bba 100644 --- a/backend/ruoyi-admin/src/main/resources/application.yml +++ b/backend/ruoyi-admin/src/main/resources/application.yml @@ -313,7 +313,7 @@ h5: # 黑名单封禁时长(分钟) blacklist-minutes: 30 # 连续验证失败多少次后加入黑名单 - blacklist-trigger-count: 5 + blacklist-trigger-count: 10 # ============================================================ # H5微信扫码登录配置 diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5AuthController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5AuthController.java index 39741a9..ca1572f 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5AuthController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5AuthController.java @@ -387,4 +387,18 @@ public class H5AuthController { ((org.dromara.pangu.h5.service.impl.H5AuthServiceImpl) authService).handleWechatCallback(code, state); return R.ok(); } + + /** + * 【调试用】清除手机号的短信发送限制(生产环境应删除此接口) + */ + @Operation(summary = "清除短信限制(调试用)", description = "清除指定手机号的短信发送限制,仅用于开发调试") + @DeleteMapping("/debug/sms-limit/{phone}") + public R 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(); + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5MemberController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5MemberController.java index 36478d1..fd9aa8c 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5MemberController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/controller/H5MemberController.java @@ -325,4 +325,26 @@ public class H5MemberController { memberService.unbindStudent(studentId); 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> getLogs( + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize) { + return R.ok(memberService.getMemberLogs(pageNum, pageSize)); + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5MemberLogVo.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5MemberLogVo.java new file mode 100644 index 0000000..fb5b5ca --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5MemberLogVo.java @@ -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; +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/H5MemberService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/H5MemberService.java index 20e2862..a80a747 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/H5MemberService.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/H5MemberService.java @@ -76,4 +76,9 @@ public interface H5MemberService { * 解绑学生 */ void unbindStudent(Long studentId); + + /** + * 获取会员操作日志 + */ + com.baomidou.mybatisplus.extension.plugins.pagination.Page getMemberLogs(int pageNum, int pageSize); } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java index 287895c..9b606a2 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java @@ -54,6 +54,7 @@ import java.util.LinkedHashMap; public class H5AuthServiceImpl implements H5AuthService { private final PgMemberMapper memberMapper; + private final org.dromara.pangu.member.service.IPgMemberLogService memberLogService; private final H5SmsProperties smsProperties; private final H5WechatProperties wechatProperties; @@ -408,6 +409,12 @@ public class H5AuthServiceImpl implements H5AuthService { vo.setPhone(maskPhone(member.getPhone())); 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; } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java index 44ed6b6..5089380 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java @@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.crypto.digest.BCrypt; 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.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.H5StudentVo; 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.PgMemberLog; 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.PgMemberStudentMapper; +import org.dromara.pangu.member.service.IPgMemberLogService; import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchoolClass; import org.dromara.pangu.school.domain.PgSchoolGrade; @@ -65,6 +70,7 @@ public class H5MemberServiceImpl implements H5MemberService { private final PgRegionMapper regionMapper; private final PgEducationMapper educationMapper; private final PgMemberStudentMapper memberStudentMapper; + private final IPgMemberLogService memberLogService; @Override public H5MemberInfoVo getMemberInfo() { @@ -114,6 +120,10 @@ public class H5MemberServiceImpl implements H5MemberService { memberMapper.updateById(member); log.info("H5会员信息修改: memberId={}", memberId); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.USER_INFO.getCode(), + MemberOperType.USER_INFO.getTitle(), "修改个人信息"); } @Override @@ -138,6 +148,10 @@ public class H5MemberServiceImpl implements H5MemberService { member.setPassword(BCrypt.hashpw(dto.getNewPassword())); memberMapper.updateById(member); log.info("H5会员密码修改: memberId={}", memberId); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.PASSWORD.getCode(), + MemberOperType.PASSWORD.getTitle(), "密码修改成功"); } @Override @@ -168,6 +182,10 @@ public class H5MemberServiceImpl implements H5MemberService { educationMapper.insert(education); log.info("H5添加教育身份: memberId={}, educationId={}", memberId, education.getEducationId()); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.EDU_ADD.getCode(), + MemberOperType.EDU_ADD.getTitle(), "学校:" + school.getSchoolName()); } @Override @@ -191,8 +209,11 @@ public class H5MemberServiceImpl implements H5MemberService { education.setSchoolClassId(dto.getSchoolClassId()); education.setSubjectId(dto.getSubjectId()); educationMapper.updateById(education); - log.info("H5修改教育身份: memberId={}, educationId={}", memberId, educationId); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.EDU_EDIT.getCode(), + MemberOperType.EDU_EDIT.getTitle(), "学校:" + school.getSchoolName()); } @Override @@ -228,6 +249,10 @@ public class H5MemberServiceImpl implements H5MemberService { // 逻辑删除 educationMapper.deleteById(educationId); log.info("H5删除教育身份: memberId={}, educationId={}", memberId, educationId); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.EDU_DELETE.getCode(), + MemberOperType.EDU_DELETE.getTitle(), "解绑教育身份"); } @Override @@ -253,8 +278,11 @@ public class H5MemberServiceImpl implements H5MemberService { // 设置当前为默认 education.setIsDefault("1"); educationMapper.updateById(education); - 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.setRelation(dto.getRelation()); memberStudentMapper.insert(ms); - log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId()); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.CHILD_ADD.getCode(), + MemberOperType.CHILD_ADD.getTitle(), "学生:" + dto.getStudentName()); } @Override @@ -403,6 +434,10 @@ public class H5MemberServiceImpl implements H5MemberService { } log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId); + + // 记录操作日志 + memberLogService.log(memberId, MemberOperType.CHILD_EDIT.getCode(), + MemberOperType.CHILD_EDIT.getTitle(), "学生:" + dto.getStudentName()); } @Override @@ -422,8 +457,11 @@ public class H5MemberServiceImpl implements H5MemberService { // 删除关联关系 memberStudentMapper.deleteById(relation.getId()); - 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 getMemberLogs(int pageNum, int pageSize) { + Long memberId = getCurrentMemberId(); + Page logPage = memberLogService.queryByMemberId(memberId, pageNum, pageSize); + + // 转换为VO + Page 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; + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberLog.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberLog.java new file mode 100644 index 0000000..3fac096 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberLog.java @@ -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; +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/enums/MemberOperType.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/enums/MemberOperType.java new file mode 100644 index 0000000..8508a37 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/enums/MemberOperType.java @@ -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; + } +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberLogMapper.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberLogMapper.java new file mode 100644 index 0000000..9cb1be5 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberLogMapper.java @@ -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 { +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberLogService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberLogService.java new file mode 100644 index 0000000..1b3c34c --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberLogService.java @@ -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 queryByMemberId(Long memberId, int pageNum, int pageSize); +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberLogServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberLogServiceImpl.java new file mode 100644 index 0000000..245299a --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberLogServiceImpl.java @@ -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 queryByMemberId(Long memberId, int pageNum, int pageSize) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PgMemberLog::getMemberId, memberId) + .orderByDesc(PgMemberLog::getOperTime); + return memberLogMapper.selectPage(page, wrapper); + } +} diff --git a/backend/sql/pg_member_log.sql b/backend/sql/pg_member_log.sql new file mode 100644 index 0000000..b47156b --- /dev/null +++ b/backend/sql/pg_member_log.sql @@ -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='会员操作日志表';