From 29b73c1f674a846ac971b98187f412c7736c6d86 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Fri, 29 May 2026 14:25:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=89=8D=E5=8F=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0=E9=98=B2?= =?UTF-8?q?XSS=E6=B3=A8=E5=85=A5=EF=BC=8C=E5=8A=A0=E5=85=A5ES=E6=90=9C?= =?UTF-8?q?=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage/member/dto/SearchMemberDto.java | 4 +- .../member/dto/UpdateMemberInfoDto.java | 20 +-- .../gym/manage/member/entity/BaseEntity.java | 3 + .../gym/manage/member/entity/Member.java | 9 +- .../manage/member/entity/SignInRecord.java | 38 ----- .../gym/manage/member/enums/GenderEnum.java | 37 +++++ .../gym/manage/member/es/entity/MemberES.java | 6 + .../es/repository/MemberESRepository.java | 5 +- .../manage/member/handler/MemberHandler.java | 53 ++++--- .../member/repository/IMemberRepository.java | 41 +++++- .../MemberCardRecordRepository.java | 2 +- .../manage/member/service/MemberService.java | 18 +++ .../service/impl/MemberServiceImpl.java | 132 +++++++++++++++--- .../impl/RefundApplicationServiceImpl.java | 7 +- .../service/impl/WechatAuthServiceImpl.java | 11 +- .../impl/WechatOfficialServiceImpl.java | 5 +- .../gym/manage/member/util/AesUtil.java | 41 ++++-- .../manage/member/util/WechatPhoneUtil.java | 2 +- .../manage/member/vo/MemberCardInfoVO.java | 92 ++++++++++++ .../manage/member/vo/MemberCardRecordVO.java | 4 +- .../gym/manage/member/vo/MemberDetailVO.java | 96 +++++++++++++ .../gym/manage/member/vo/MemberInfoVO.java | 13 +- .../src/main/resources/db/schema.sql | 2 +- .../gym/manage/app/ManageApplication.java | 10 +- .../manage/common/util/HtmlEscapeUtil.java | 117 ++++++++++++++++ 25 files changed, 635 insertions(+), 133 deletions(-) delete mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java create mode 100644 gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java index bcd528d..8b66ee0 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java @@ -12,8 +12,8 @@ public class SearchMemberDto { // 搜索字段 - 包括 会员号、昵称、手机号 private String searchValue; - // 排序 - private String filter; + // 性别排序 + private Integer gender; // 页码 private Integer pageNum = 1; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java index 835347c..fb633df 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java @@ -1,30 +1,32 @@ package cn.novalon.gym.manage.member.dto; +import cn.novalon.gym.manage.member.enums.GenderEnum; import lombok.Data; +import java.time.LocalDate; import java.util.Date; /** * 更新会员信息Dto - * + * * @author 付嘉 * @date 2026-05-10 */ @Data public class UpdateMemberInfoDto { - + // 昵称 private String nickname; - + // 性别 - private Integer gender; - + private GenderEnum gender; + // 生日 - private Date birthday; - + private LocalDate birthday; + // 头像 private String avatar; - + // 地址 private String address; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java index 1ad7597..865e4bb 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java @@ -33,6 +33,9 @@ public abstract class BaseEntity implements Persistable { @Column("updated_at") private LocalDateTime updatedAt; + @Column("deleted_at") + private LocalDateTime deletedAt; + // 判断当前实体是否是新建的 @Override public boolean isNew() { diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java index 36c7ba4..8983e20 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -1,13 +1,10 @@ package cn.novalon.gym.manage.member.entity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Date; @@ -44,7 +41,7 @@ public class Member extends BaseEntity { //生日 @Column("birthday") - private Date birthday; + private LocalDate birthday; //地址 @Column("address") diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java deleted file mode 100644 index 83dae3a..0000000 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.novalon.gym.manage.member.entity; - -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.Table; - -import java.time.LocalDate; -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Table("sign_in_record") -public class SignInRecord { - - @Id - private Long id; - - // 会员ID - @Column("member_id") - private Long memberId; - - // 签到日期 - @Column("sign_in_date") - private LocalDate signInDate; - - // 签到时间 - @Column("sign_in_time") - private LocalDateTime signInTime; - - // 创建时间 - @CreatedDate - @Column("created_at") - private LocalDateTime createdAt; -} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java new file mode 100644 index 0000000..9924cfb --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java @@ -0,0 +1,37 @@ +package cn.novalon.gym.manage.member.enums; + +import lombok.Getter; + +/** + * 性别枚举 + * + * @author 付嘉 + * @date 2026-05-29 + */ +@Getter +public enum GenderEnum { + + UNKNOWN(0, "未知"), + MALE(1, "男"), + FEMALE(2, "女"); + + private final Integer code; + private final String desc; + + GenderEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + public static GenderEnum fromCode(Integer code) { + if (code == null) { + return UNKNOWN; + } + for (GenderEnum gender : values()) { + if (gender.code.equals(code)) { + return gender; + } + } + return UNKNOWN; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java index b479ce5..1e2e23b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java @@ -1,12 +1,18 @@ package cn.novalon.gym.manage.member.es.entity; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import java.time.LocalDateTime; + @Data +@NoArgsConstructor +@AllArgsConstructor @Document(indexName = "gym_members") public class MemberES { diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java index a233097..80064ed 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java @@ -2,6 +2,7 @@ package cn.novalon.gym.manage.member.es.repository; import cn.novalon.gym.manage.member.es.entity.MemberES; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -15,6 +16,6 @@ public interface MemberESRepository extends ReactiveElasticsearchRepository findByMemberNoOrPhoneOrNicknameContainingAndGender( - String memberNo, String phone, String nickname,String gender, Pageable pageable); + Flux findByMemberNoOrPhoneOrNicknameContaining( + String memberNo, String phone, String nickname, Pageable pageable); } \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 7571053..6d1206b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -8,6 +8,7 @@ import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.sys.util.AuthUtil; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; @@ -34,8 +35,6 @@ public class MemberHandler { private final MemberService memberService; private final WechatAuthService wechatAuthService; private final WechatOfficialService wechatOfficialService; - private final JwtTokenProvider jwtTokenProvider; - private final WechatProperties wechatProperties; private final AuthUtil authUtil; /** @@ -179,11 +178,21 @@ public class MemberHandler { log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId); - // TODO 多表查询:会员信息、团课信息、会员卡信息 - - return ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue("成功"); + return memberService.getMemberDetail(memberId) + .flatMap(detail -> { + if (detail.getPhone() != null && !detail.getPhone().isEmpty()) { + try { + String decryptedPhone = AesUtil.decrypt(detail.getPhone()); + detail.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone)); + } catch (Exception e) { + log.error("手机号解密失败, memberId: {}", detail.getId(), e); + detail.setPhone(null); + } + } + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(detail); + }); } /** @@ -201,13 +210,14 @@ public class MemberHandler { long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误"); + // TODO: 补充签到记录 log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId); - // TODO 多表查询:会员信息、团课信息、会员卡信息 - - return ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue("成功"); + return request.bodyToMono(UpdateMemberInfoDto.class) + .flatMap(updateDto -> memberService.adminUpdateMemberInfo(memberId, updateDto)) + .flatMap(detail -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(detail)); } /** @@ -221,9 +231,9 @@ public class MemberHandler { Long adminId = authUtil.getMemberIdOrThrow(request); String keyword = request.queryParam("searchValue").orElse(null); - String filter = request.queryParam("filter").orElse(null); - int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); - int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + Integer filter = NumberUtils.toInt(request.queryParam("filter").orElse("-1"), -1); + Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); + Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", adminId, keyword, filter, pageNum, pageSize); @@ -233,10 +243,8 @@ public class MemberHandler { // 解密手机号 if (member.getPhone() != null && !member.getPhone().isEmpty()) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); - member.setPhone(decryptedPhone); + String decryptedPhone = AesUtil.decrypt(member.getPhone()); + member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone)); } catch (Exception e) { log.error("手机号解密失败, memberId: {}", member.getId(), e); member.setPhone(null); @@ -263,16 +271,15 @@ public class MemberHandler { int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize); + // TODO: 补充签到记录 return memberService.findAll(pageNum, pageSize) .map(member -> { // 解密手机号 if (member.getPhone() != null && !member.getPhone().isEmpty()) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); - member.setPhone(decryptedPhone); + String decryptedPhone = AesUtil.decrypt(member.getPhone()); + member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone)); } catch (Exception e) { log.error("手机号解密失败, memberId: {}", member.getId(), e); member.setPhone(null); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java index e746aea..d6c868e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java @@ -1,7 +1,9 @@ package cn.novalon.gym.manage.member.repository; import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.vo.MemberCardInfoVO; import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -30,7 +32,44 @@ public interface IMemberRepository extends R2dbcRepository { /** * 分页查询所有会员 - * 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有 */ Flux findAllBy(Pageable pageable); + + /** + * 查询会员的所有卡片 + */ + @Query("SELECT " + + " r.id, " + + " r.member_card_record_id, " + + " r.member_id, " + + " r.member_card_id, " + + " r.status, " + + " r.remaining_times, " + + " r.remaining_amount, " + + " r.expire_time, " + + " r.purchase_time, " + + " r.source_order_id, " + + " r.created_at, " + + " r.updated_at, " + + " r.version, " + + " r.card_composition, " + + " c.id AS card_id, " + + " c.member_card_id, " + + " c.member_card_name, " + + " c.member_card_type, " + + " c.member_card_price, " + + " c.member_card_validity_days, " + + " c.member_card_total_times, " + + " c.member_card_amount, " + + " c.member_card_status, " + + " c.extra_config, " + + " c.created_at AS card_created_at, " + + " c.updated_at AS card_updated_at " + + "FROM member_card_record r " + + "LEFT JOIN member_card c ON r.member_card_id = c.id " + + "WHERE r.member_id = :memberId " + + "AND r.deleted_at IS NULL " + + "AND c.deleted_at IS NULL " + + "ORDER BY r.created_at DESC") + Flux findCardRecordsWithCardInfoByMemberId(Long memberId); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java index b3ce72e..8c3d6f1 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java @@ -13,7 +13,7 @@ import java.time.LocalDateTime; /** * 会员卡记录 Repository(会员持有的卡) - * + * * @author 付嘉 * @date 2026-05-27 */ diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java index c659f1b..72e4b1f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.entity.Member; import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -58,4 +59,21 @@ public interface MemberService { * @return 所有会员信息 */ Flux findAll(Integer pageNum, Integer pageSize); + + /** + * 前台管理端获取会员详情(含会员卡信息) + * + * @param memberId 会员ID + * @return 会员详情 + */ + Mono getMemberDetail(Long memberId); + + /** + * 前台管理端编辑会员信息 + * + * @param memberId 会员ID + * @param updateDto 更新信息DTO + * @return 更新后的会员详情 + */ + Mono adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index d1b6c44..e63e79d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -4,20 +4,31 @@ import cn.novalon.gym.manage.common.exception.ConflictException; import cn.novalon.gym.manage.common.exception.ErrorCode; import cn.novalon.gym.manage.common.exception.NotFoundException; import cn.novalon.gym.manage.common.exception.SystemException; -import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.enums.GenderEnum; +import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.es.entity.MemberES; import cn.novalon.gym.manage.member.es.repository.MemberESRepository; import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.BeanConvertUtil; import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.vo.MemberCardInfoVO; +import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.IndexResponse; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -26,6 +37,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * 会员服务实现 @@ -41,7 +56,6 @@ public class MemberServiceImpl implements MemberService { private final IMemberRepository memberRepository; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; - private final WechatProperties wechatProperties; private EsSyncUtils.EntitySyncer memberSyncer; @@ -67,10 +81,10 @@ public class MemberServiceImpl implements MemberService { return memberRepository.findById(memberId) .flatMap(member -> { if (updateDto.getNickname() != null) { - member.setNickname(updateDto.getNickname()); + member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); } if (updateDto.getGender() != null) { - member.setGender(updateDto.getGender()); + member.setGender(updateDto.getGender().getCode()); } if (updateDto.getBirthday() != null) { member.setBirthday(updateDto.getBirthday()); @@ -79,7 +93,7 @@ public class MemberServiceImpl implements MemberService { member.setAvatar(updateDto.getAvatar()); } if (updateDto.getAddress() != null) { - member.setAddress(updateDto.getAddress()); + member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress())); } return memberRepository.save(member); @@ -99,11 +113,14 @@ public class MemberServiceImpl implements MemberService { String phone = member.getPhone(); String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null; + GenderEnum genderEnum = GenderEnum.fromCode(member.getGender()); + return MemberInfoVO.builder() .id(member.getId()) .nickname(member.getNickname()) .phone(maskedPhone) - .gender(member.getGender()) + .gender(genderEnum) + .genderDesc(genderEnum.getDesc()) .birthday(member.getBirthday()) .avatar(member.getAvatar()) .hasPhone(phone != null) @@ -117,9 +134,7 @@ public class MemberServiceImpl implements MemberService { String encryptedPhone; try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - encryptedPhone = AesUtil.encrypt(phone, secretKey, iv); + encryptedPhone = AesUtil.encrypt(phone); log.info("手机号加密成功"); } catch (Exception e) { log.error("手机号加密失败", e); @@ -147,7 +162,7 @@ public class MemberServiceImpl implements MemberService { public Flux searchMember(SearchMemberDto searchMemberDto) { log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", searchMemberDto.getSearchValue(), - searchMemberDto.getFilter(), + searchMemberDto.getGender(), searchMemberDto.getPageNum(), searchMemberDto.getPageSize()); @@ -155,22 +170,23 @@ public class MemberServiceImpl implements MemberService { if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ log.debug("搜索值为手机号格式,进行加密处理"); - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - searchValue = AesUtil.encrypt(searchValue,secretKey,iv); + searchValue = AesUtil.encrypt(searchValue); } Pageable pageable = PageRequest.of( searchMemberDto.getPageNum() - 1, - searchMemberDto.getPageSize(), - Sort.by(Sort.Direction.DESC, "update_at") + searchMemberDto.getPageSize() ); - return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender( + if (searchValue == null) { + log.warn("搜索值为空,返回空结果"); + return Flux.empty(); + } + + return memberESRepository.findByMemberNoOrPhoneOrNicknameContaining( searchValue, searchValue, searchValue, - searchMemberDto.getFilter() , pageable ); } @@ -187,6 +203,86 @@ public class MemberServiceImpl implements MemberService { return memberRepository.findAllBy(pageable); } + @Override + public Mono getMemberDetail(Long memberId) { + log.info("查询会员详情, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .zipWith( + memberRepository.findCardRecordsWithCardInfoByMemberId(memberId) + .collectList(), + (baseInfo, cardList) -> { + MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class); + + GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender()); + memberDetailVO.setGenderDesc(genderEnum.getDesc()); + + List enrichedCards = cardList.stream() + .peek(vo -> { + if (vo.getMemberCardType() != null) { + try { + MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType()); + vo.setMemberCardTypeDesc(cardType.getDesc()); + } catch (IllegalArgumentException e) { + vo.setMemberCardTypeDesc(vo.getMemberCardType()); + } + } + if (vo.getMemberCardStatus() != null) { + vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架"); + } + }) + .collect(Collectors.toList()); + memberDetailVO.setMemberCards(enrichedCards); + + long activeCount = enrichedCards.stream() + .filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) + .count(); + memberDetailVO.setActiveCardCount((int) activeCount); + memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount); + + return memberDetailVO; + } + ); + } + + + @Override + public Mono adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) { + log.info("前台管理端编辑会员信息, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })) + .flatMap(member -> { + log.error("有用户"); + if (updateDto.getNickname() != null) { + member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); + } + if (updateDto.getGender() != null) { + member.setGender(updateDto.getGender().getCode()); + } + if (updateDto.getBirthday() != null) { + member.setBirthday(updateDto.getBirthday()); + } + if (updateDto.getAvatar() != null) { + member.setAvatar(updateDto.getAvatar()); + } + if (updateDto.getAddress() != null) { + member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress())); + } + + return memberRepository.save(member); + }) + .doOnSuccess(memberSyncer::sync) + .map(savedMember -> true) + .onErrorResume(e -> { + log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e); + return Mono.just(false); + }); + } + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) .flatMap(member -> { @@ -205,4 +301,4 @@ public class MemberServiceImpl implements MemberService { throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java index 7097f3e..f492e78 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.entity.RefundApplication; import cn.novalon.gym.manage.member.enums.RefundStatus; import cn.novalon.gym.manage.member.repository.RefundApplicationRepository; @@ -38,7 +39,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { .then(Mono.defer(() -> { RefundApplication application = RefundApplication.builder() .recordId(recordId) - .reason(reason) + .reason(HtmlEscapeUtil.escape(reason)) .status(RefundStatus.PENDING) .applyTime(LocalDateTime.now()) .build(); @@ -58,7 +59,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); } - return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark) + return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark)) .thenReturn(application) .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId)); @@ -74,7 +75,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); } - return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark) + return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark)) .thenReturn(application) .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId)); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 5e7f299..9d567d0 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ConflictException; import cn.novalon.gym.manage.common.exception.ErrorCode; import cn.novalon.gym.manage.common.exception.NotFoundException; import cn.novalon.gym.manage.common.exception.SystemException; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.WechatLoginDto; import cn.novalon.gym.manage.member.entity.Member; @@ -42,7 +43,6 @@ public class WechatAuthServiceImpl implements WechatAuthService { private final WechatApiService wechatApiService; private final IMemberRepository memberRepository; - private final WechatProperties wechatProperties; private final WechatPhoneUtil wechatPhoneUtil; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; @@ -199,10 +199,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { private String encryptPhone(String phoneNumber) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - - String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv); + String encryptedPhone = AesUtil.encrypt(phoneNumber); log.debug("手机号加密成功"); return encryptedPhone; @@ -214,10 +211,8 @@ public class WechatAuthServiceImpl implements WechatAuthService { public String decryptPhone(String encryptedPhone) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv); + String phoneNumber = AesUtil.decrypt(encryptedPhone); log.debug("手机号解密成功"); return phoneNumber; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index b5b863a..5143d9b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.entity.Member; import cn.novalon.gym.manage.member.es.entity.MemberES; @@ -74,11 +75,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setOfficialOpenId(openId); if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) { - existingMember.setNickname(userInfo.getNickname()); + existingMember.setNickname(HtmlEscapeUtil.escape(userInfo.getNickname())); } if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) { - existingMember.setAvatar(userInfo.getHeadimgurl()); + existingMember.setAvatar(HtmlEscapeUtil.escape(userInfo.getHeadimgurl())); } return memberRepository.save(existingMember) diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java index f54a003..f5fd4af 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java @@ -1,12 +1,24 @@ package cn.novalon.gym.manage.member.util; +import cn.novalon.gym.manage.member.config.WechatProperties; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.IndexResponse; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.checkerframework.checker.units.qual.K; +import org.elasticsearch.client.RestClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.HashMap; +import java.util.Map; /** * AES加密工具类 @@ -16,24 +28,35 @@ import java.util.Base64; */ @Slf4j +@Component public class AesUtil { private static final String ALGORITHM = "AES"; private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; + private static String KEY; + private static String IV; + + @Autowired + public void setWechatProperties(WechatProperties props) { + KEY = props.getPhoneEncryption().getSecretKey(); // 从配置类读取 + IV = props.getPhoneEncryption().getIv(); + if(KEY == null || IV == null) throw new RuntimeException("请配置AES密钥和偏移量"); + } + + /** * AES 解密 * * @param encryptedData 加密数据,Base64编码 - * @param key AES密钥,Base64编码(32字节) - * @param iv 初始化向量IV,Base64编码(16字节) * @return 解密后的字符串 */ - public static String decrypt(String encryptedData, String key, String iv) { + public static String decrypt(String encryptedData) { + try { byte[] dataByte = Base64.getDecoder().decode(encryptedData); - byte[] keyByte = Base64.getDecoder().decode(key); - byte[] ivByte = Base64.getDecoder().decode(iv); + byte[] keyByte = Base64.getDecoder().decode(KEY); + byte[] ivByte = Base64.getDecoder().decode(IV); Cipher cipher = Cipher.getInstance(TRANSFORMATION); SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); @@ -52,15 +75,13 @@ public class AesUtil { * AES 加密 * * @param data 原始数据 - * @param key AES密钥,Base64编码(32字节) - * @param iv 初始化向量IV,Base64编码(16字节) * @return Base64编码的加密数据 */ - public static String encrypt(String data, String key, String iv) { + public static String encrypt(String data) { try { byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); - byte[] keyByte = Base64.getDecoder().decode(key); - byte[] ivByte = Base64.getDecoder().decode(iv); + byte[] keyByte = Base64.getDecoder().decode(KEY); + byte[] ivByte = Base64.getDecoder().decode(IV); Cipher cipher = Cipher.getInstance(TRANSFORMATION); SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java index e4f3c4e..a9c6c55 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java @@ -56,7 +56,7 @@ public class WechatPhoneUtil { * @param phone 手机号 * @return 脱敏后的手机号,如:138****8000 */ - private String maskPhone(String phone) { + public static String maskPhone(String phone) { if (phone == null || phone.length() < 7) { return "***"; } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java new file mode 100644 index 0000000..785f890 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java @@ -0,0 +1,92 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 会员卡类型响应 VO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberCardInfoVO { + + /** + * 主键ID + */ + private Long id; + + /** + * 会员卡ID + */ + private Long memberCardId; + + /** + * 会员卡名称 + */ + private String memberCardName; + + /** + * 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡 + */ + private String memberCardType; + + /** + * 卡类型描述 + */ + private String memberCardTypeDesc; + + /** + * 会员卡价格 + */ + private BigDecimal memberCardPrice; + + /** + * 有效天数(时长卡用) + */ + private Integer memberCardValidityDays; + + /** + * 总次数(次卡用) + */ + private Integer memberCardTotalTimes; + + /** + * 面额(储值卡用) + */ + private BigDecimal memberCardAmount; + + /** + * 状态:0-下架, 1-上架 + */ + private Integer memberCardStatus; + + /** + * 状态描述 + */ + private String memberCardStatusDesc; + + /** + * 扩展配置(JSON格式) + */ + private String extraConfig; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java index db4aed9..966590d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; /** * 会员卡记录响应 VO - * + * * @author 付嘉 * @date 2026-05-27 */ @@ -68,4 +68,4 @@ public class MemberCardRecordVO { * 创建时间 */ private LocalDateTime createdAt; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java new file mode 100644 index 0000000..51ea67c --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java @@ -0,0 +1,96 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +/** + * 会员详情 VO(管理端使用) + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberDetailVO { + + // ==================== 会员基础信息 ==================== + + /** + * 会员ID + */ + private Long id; + + /** + * 会员编号 + */ + private String memberNo; + + /** + * 昵称 + */ + private String nickname; + + /** + * 手机号(脱敏显示) + */ + private String phone; + + /** + * 性别描述 + */ + private String genderDesc; + + /** + * 生日 + */ + private Date birthday; + + /** + * 地址 + */ + private String address; + + /** + * 头像URL + */ + private String avatar; + + /** + * 是否关注服务号 + */ + private Boolean subscribed; + + /** + * 最后登录时间 + */ + private LocalDateTime lastLoginAt; + + /** + * 注册时间 + */ + private LocalDateTime createdAt; + + // ==================== 会员卡信息 ==================== + + /** + * 会员持有的卡列表 + */ + private List memberCards; + + /** + * 有效会员卡数量 + */ + private Integer activeCardCount; + + /** + * 过期/用完会员卡数量 + */ + private Integer inactiveCardCount; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java index d8b60e4..f3e763d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java @@ -1,15 +1,17 @@ package cn.novalon.gym.manage.member.vo; +import cn.novalon.gym.manage.member.enums.GenderEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.util.Date; /** * 会员信息 VO - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -30,10 +32,13 @@ public class MemberInfoVO { private String phone; // 性别 - private Integer gender; + private GenderEnum gender; + + // 性别描述 + private String genderDesc; // 生日 - private Date birthday; + private LocalDate birthday; // 头像 private String avatar; @@ -43,4 +48,4 @@ public class MemberInfoVO { // 是否已关注公众号 private Boolean isSubscribed; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/resources/db/schema.sql b/gym-manage-api/gym-member/src/main/resources/db/schema.sql index 2eea8da..b76f2df 100644 --- a/gym-manage-api/gym-member/src/main/resources/db/schema.sql +++ b/gym-manage-api/gym-member/src/main/resources/db/schema.sql @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS member_user ( nickname VARCHAR(100), -- 昵称 phone VARCHAR(255), -- 手机号(AES加密存储) gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女 - birthday TIMESTAMP, -- 生日 + birthday DATE, -- 生日 address VARCHAR(500), -- 地址 avatar VARCHAR(500), -- 头像URL subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号 diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index 0278f53..a87aaef 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -10,6 +10,8 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.web.server.WebFilter; @@ -17,9 +19,13 @@ import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) -@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", +@EnableR2dbcRepositories(basePackages = { + "cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository" , - "cn.novalon.gym.manage.gymmembercard.dao"}) + "cn.novalon.gym.manage.gymmembercard.dao", + "cn.novalon.gym.manage.member.repository" +}) +@EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository") public class ManageApplication { private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java new file mode 100644 index 0000000..63fbafb --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java @@ -0,0 +1,117 @@ +package cn.novalon.gym.manage.common.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * HTML 转义工具类 + * 防止 XSS 注入攻击 + * + * @author 付嘉 + * @date 2026-05-29 + */ +public class HtmlEscapeUtil { + + private static final Map ESCAPE_MAP = new HashMap<>(); + private static final Map UNESCAPE_MAP = new HashMap<>(); + private static final Pattern HTML_PATTERN = Pattern.compile("<[^>]*>"); + + static { + // HTML 特殊字符转义映射 + ESCAPE_MAP.put('&', "&"); + ESCAPE_MAP.put('<', "<"); + ESCAPE_MAP.put('>', ">"); + ESCAPE_MAP.put('"', """); + ESCAPE_MAP.put('\'', "'"); + + // 反向映射 + UNESCAPE_MAP.put("&", '&'); + UNESCAPE_MAP.put("<", '<'); + UNESCAPE_MAP.put(">", '>'); + UNESCAPE_MAP.put(""", '"'); + UNESCAPE_MAP.put("'", '\''); + } + + /** + * 转义 HTML 特殊字符 + * + * @param input 原始字符串 + * @return 转义后的字符串 + */ + public static String escape(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + StringBuilder result = new StringBuilder(); + for (char c : input.toCharArray()) { + String escaped = ESCAPE_MAP.get(c); + if (escaped != null) { + result.append(escaped); + } else { + result.append(c); + } + } + return result.toString(); + } + + /** + * 反转义 HTML 特殊字符 + * + * @param input 转义后的字符串 + * @return 原始字符串 + */ + public static String unescape(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + String result = input; + for (Map.Entry entry : UNESCAPE_MAP.entrySet()) { + result = result.replace(entry.getKey(), String.valueOf(entry.getValue())); + } + return result; + } + + /** + * 移除所有 HTML 标签 + * + * @param input 原始字符串 + * @return 移除标签后的字符串 + */ + public static String stripHtmlTags(String input) { + if (input == null || input.isEmpty()) { + return input; + } + return HTML_PATTERN.matcher(input).replaceAll(""); + } + + /** + * 安全转义(转义 + 移除标签) + * + * @param input 原始字符串 + * @return 安全字符串 + */ + public static String sanitize(String input) { + if (input == null || input.isEmpty()) { + return input; + } + // 先移除 HTML 标签,再转义特殊字符 + String noTags = stripHtmlTags(input); + return escape(noTags); + } + + /** + * 判断是否包含 HTML 标签 + * + * @param input 原始字符串 + * @return true-包含, false-不包含 + */ + public static boolean containsHtmlTags(String input) { + if (input == null || input.isEmpty()) { + return false; + } + return HTML_PATTERN.matcher(input).find(); + } +} \ No newline at end of file