feat(user): 新增邮箱注册与验证码发送功能
- 新增 UserRegisterDTO 及 /user/register 接口 - 集成 MailerSend,异步发送 6 位验证码邮件 - 添加 RedisUtil 缓存验证码 10 分钟 - 补充 SEND_MAIL_FAILED、CONFIRM_PASSWORD_NOT_MATCH 错误码 - 关闭 Spring Security CSRF 与表单登录,放行 /user/register - AppleService 移除 @AllArgsConstructor,改用 @Resource 注入
This commit is contained in:
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- Entrypoint `src/main/java/com/yolo/keyborad/MyApplication.java`; feature code organized by layer: `controller` (REST), `service` (business), `mapper` (MyBatis mappers), `model`/`common`/`constant` for DTOs, responses, and constants, plus `config`, `aop`, `annotation`, `Interceptor`, and `utils` for cross-cutting concerns.
|
||||||
|
- Resource configs live in `src/main/resources`: `application.yml` with `application-dev.yml`/`application-prod.yml` profiles, mapper XML files under `mapper/`, and platform keys/certs (Apple, mail, storage). Keep secrets out of commits.
|
||||||
|
- Tests belong in `src/test/java/com/yolo/keyborad/...` mirroring package names; add fixtures alongside tests when needed.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
- `./mvnw clean install` — full build with tests; requires JDK 17.
|
||||||
|
- `./mvnw test` — run test suite only.
|
||||||
|
- `./mvnw spring-boot:run -Dspring-boot.run.profiles=dev` — start the API with the dev profile (loads `application-dev.yml`).
|
||||||
|
- `./mvnw clean package -DskipTests` — create an artifact when tests are already covered elsewhere.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- Java 17, Spring Boot 3.5, MyBatis/MyBatis-Plus; prefer Lombok for boilerplate (`@Data`, `@Builder`) and constructor injection for services.
|
||||||
|
- Use 4-space indentation, lowercase package names, `UpperCamelCase` for classes, `lowerCamelCase` for fields/params.
|
||||||
|
- Controllers end with `*Controller`, services with `*Service`, mapper interfaces with `*Mapper`, and request/response DTOs under `model` or `common` with clear suffixes like `Request`/`Response`.
|
||||||
|
- Keep configuration isolated in `config`; shared constants in `constant`; AOP/logging in `aop`; custom annotations in `annotation`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Use Spring Boot Test + JUnit (via `spring-boot-starter-test`, JUnit 4/5 support) and MockMvc/WebTestClient for HTTP layers when practical.
|
||||||
|
- Name classes `*Test` and align packages with the code under test. Cover service logic, mappers, and controller contracts (status + payload shape).
|
||||||
|
- For data-access tests, use in-memory setups or dedicated test containers and clean up test data.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Follow the existing conventional style seen in history (e.g., `feat(user): add email registration`); keep scope lowercase and concise.
|
||||||
|
- PRs should describe the change, list validation steps/commands run, call out config/profile impacts, and link issues/tasks. Add screenshots or sample requests/responses for API-facing changes when helpful.
|
||||||
|
- Ensure secrets (p8 certificates, Mailgun keys, AWS creds) are never committed; rely on environment variables or local config overrides.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
- Activate the intended profile via `SPRING_PROFILES_ACTIVE` or `-Dspring-boot.run.profiles`. Keep `application-dev.yml` local-only; never hardcode production endpoints or credentials.
|
||||||
|
- Validate signing/encryption helpers (`SignInterceptor`, JWT, Apple receipt validation) with representative non-production keys before merging.
|
||||||
|
- Log only necessary context; avoid logging tokens, receipts, or PII.
|
||||||
@@ -75,7 +75,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/character/listByUser",
|
"/character/listByUser",
|
||||||
"/user/updateInfo",
|
"/user/updateInfo",
|
||||||
"/user/detail",
|
"/user/detail",
|
||||||
"/user/register"
|
"/user/register",
|
||||||
|
"/character/updateUserCharacterSort"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import cn.dev33.satoken.stp.StpUtil;
|
|||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import com.yolo.keyborad.common.BaseResponse;
|
import com.yolo.keyborad.common.BaseResponse;
|
||||||
import com.yolo.keyborad.common.ResultUtils;
|
import com.yolo.keyborad.common.ResultUtils;
|
||||||
|
import com.yolo.keyborad.model.dto.userCharacter.KeyboardUserCharacterDTO;
|
||||||
|
import com.yolo.keyborad.model.dto.userCharacter.KeyboardUserCharacterSortUpdateDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardTag;
|
import com.yolo.keyborad.model.entity.KeyboardTag;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
||||||
@@ -15,10 +17,9 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.ai.chat.model.ChatModel;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -34,6 +35,8 @@ public class CharacterController {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private KeyboardCharacterService characterService;
|
private KeyboardCharacterService characterService;
|
||||||
|
@Autowired
|
||||||
|
private ChatModel chatModel;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "人设列表", description = "人设列表接口按 rank 排名")
|
@Operation(summary = "人设列表", description = "人设列表接口按 rank 排名")
|
||||||
@@ -59,7 +62,15 @@ public class CharacterController {
|
|||||||
@GetMapping("/listByUser")
|
@GetMapping("/listByUser")
|
||||||
@Operation(summary = "用户人设列表", description = "用户人设列表接口")
|
@Operation(summary = "用户人设列表", description = "用户人设列表接口")
|
||||||
public BaseResponse<List<KeyboardUserCharacterVO>> userList() {
|
public BaseResponse<List<KeyboardUserCharacterVO>> userList() {
|
||||||
List<KeyboardCharacter> keyboardCharacters = characterService.selectListByUserId();
|
return ResultUtils.success(characterService.selectListByUserId());
|
||||||
return ResultUtils.success(BeanUtil.copyToList(keyboardCharacters, KeyboardUserCharacterVO.class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/updateUserCharacterSort")
|
||||||
|
@Operation(summary = "更新用户人设排序",description = "更新用户人设排序接口")
|
||||||
|
public BaseResponse<Boolean> updateUserCharacterSort(@RequestBody KeyboardUserCharacterSortUpdateDTO sortUpdateDTO) {
|
||||||
|
characterService.updateSort(sortUpdateDTO);
|
||||||
|
return ResultUtils.success(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package com.yolo.keyborad.mapper;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
||||||
|
import com.yolo.keyborad.model.vo.character.KeyboardUserCharacterVO;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -9,4 +13,9 @@ import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public interface KeyboardUserCharacterMapper extends BaseMapper<KeyboardUserCharacter> {
|
public interface KeyboardUserCharacterMapper extends BaseMapper<KeyboardUserCharacter> {
|
||||||
|
|
||||||
|
List<KeyboardUserCharacterVO> selectByUserId(@Param("loginId") long loginId);
|
||||||
|
|
||||||
|
void updateSortByIdAndUserId(@Param("sort") Integer[] sort,@Param("userId") long userId);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.yolo.keyborad.model.dto.userCharacter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2025/12/3 15:43
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Schema
|
||||||
|
@Data
|
||||||
|
public class KeyboardUserCharacterDTO {
|
||||||
|
@Schema(description="主键 Id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description="键盘人设 id")
|
||||||
|
private Long characterId;
|
||||||
|
|
||||||
|
@Schema(description = "用户展示界面排序")
|
||||||
|
private Long sort;
|
||||||
|
|
||||||
|
@Schema(description="用户 id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除")
|
||||||
|
private Boolean deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.yolo.keyborad.model.dto.userCharacter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "用户键盘顺序更新 DTO")
|
||||||
|
@Data
|
||||||
|
public class KeyboardUserCharacterSortUpdateDTO {
|
||||||
|
|
||||||
|
@Schema(description = "用户展示界面排序")
|
||||||
|
private List<Integer> sort;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -40,4 +42,12 @@ public class KeyboardUserCharacter {
|
|||||||
@TableField(value = "user_id")
|
@TableField(value = "user_id")
|
||||||
@Schema(description="用户 id")
|
@Schema(description="用户 id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@TableField(value = "emoji")
|
||||||
|
@Schema(description = "emoji")
|
||||||
|
private String emoji;
|
||||||
|
|
||||||
|
@TableField(value = "sort")
|
||||||
|
@Schema(description = "展示顺序")
|
||||||
|
private List<Integer> sort;
|
||||||
}
|
}
|
||||||
@@ -38,4 +38,5 @@ public class KeyboardCharacterRespVO {
|
|||||||
|
|
||||||
@Schema(description="排名顺序")
|
@Schema(description="排名顺序")
|
||||||
private Integer rank;
|
private Integer rank;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -25,4 +26,6 @@ public class KeyboardUserCharacterVO {
|
|||||||
@Schema(description = "人设名")
|
@Schema(description = "人设名")
|
||||||
private String characterName;
|
private String characterName;
|
||||||
|
|
||||||
|
@Schema(description = "emoji")
|
||||||
|
private String emoji;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.yolo.keyborad.service;
|
package com.yolo.keyborad.service;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.dto.userCharacter.KeyboardUserCharacterSortUpdateDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardTag;
|
import com.yolo.keyborad.model.entity.KeyboardTag;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
||||||
|
import com.yolo.keyborad.model.vo.character.KeyboardUserCharacterVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
/*
|
/*
|
||||||
@@ -18,6 +20,7 @@ public interface KeyboardCharacterService extends IService<KeyboardCharacter>{
|
|||||||
|
|
||||||
List<KeyboardCharacter> selectListByTag(Long tagId);
|
List<KeyboardCharacter> selectListByTag(Long tagId);
|
||||||
|
|
||||||
List<KeyboardCharacter> selectListByUserId();
|
List<KeyboardUserCharacterVO> selectListByUserId();
|
||||||
|
|
||||||
|
void updateSort(KeyboardUserCharacterSortUpdateDTO sortUpdateDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||||
import com.yolo.keyborad.mapper.KeyboardCharacterMapper;
|
import com.yolo.keyborad.mapper.KeyboardCharacterMapper;
|
||||||
import com.yolo.keyborad.mapper.KeyboardUserCharacterMapper;
|
import com.yolo.keyborad.mapper.KeyboardUserCharacterMapper;
|
||||||
|
import com.yolo.keyborad.model.dto.userCharacter.KeyboardUserCharacterSortUpdateDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardUserCharacter;
|
||||||
|
import com.yolo.keyborad.model.vo.character.KeyboardUserCharacterVO;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||||
import com.yolo.keyborad.service.KeyboardCharacterService;
|
import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -44,23 +48,20 @@ public class KeyboardCharacterServiceImpl extends ServiceImpl<KeyboardCharacterM
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<KeyboardCharacter> selectListByUserId() {
|
public List<KeyboardUserCharacterVO> selectListByUserId() {
|
||||||
long loginId = StpUtil.getLoginIdAsLong();
|
long loginId = StpUtil.getLoginIdAsLong();
|
||||||
|
|
||||||
List<KeyboardUserCharacter> keyboardUserCharacters = keyboardUserCharacterMapper.selectList(new LambdaQueryWrapper<KeyboardUserCharacter>()
|
return keyboardUserCharacterMapper.selectByUserId(loginId);
|
||||||
.eq(KeyboardUserCharacter::getDeleted, false)
|
}
|
||||||
.eq(KeyboardUserCharacter::getUserId, loginId));
|
|
||||||
|
|
||||||
List<Long> characterIds = keyboardUserCharacters.stream()
|
@Override
|
||||||
.map(KeyboardUserCharacter::getCharacterId)
|
@Transactional
|
||||||
.toList();
|
public void updateSort(KeyboardUserCharacterSortUpdateDTO sortUpdateDTO) {
|
||||||
if (CollectionUtils.isEmpty(characterIds)) {
|
if (sortUpdateDTO.getSort() == null || sortUpdateDTO.getSort().isEmpty()) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
Integer[] sortArray = sortUpdateDTO.getSort().toArray(new Integer[0]);
|
||||||
return keyboardCharacterMapper.selectList(new LambdaQueryWrapper<KeyboardCharacter>()
|
long loginIdAsLong = StpUtil.getLoginIdAsLong();
|
||||||
.eq(KeyboardCharacter::getDeleted, false)
|
keyboardUserCharacterMapper.updateSortByIdAndUserId(sortArray, loginIdAsLong);
|
||||||
.in(KeyboardCharacter::getId, characterIds));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.yolo.keyborad.mapper.KeyboardUserCharacterMapper">
|
<mapper namespace="com.yolo.keyborad.mapper.KeyboardUserCharacterMapper">
|
||||||
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardUserCharacter">
|
|
||||||
<!--@Table keyboard_user_character-->
|
<resultMap id="KeyboardUserCharacterVOMap"
|
||||||
<id column="id" jdbcType="BIGINT" property="id" />
|
type="com.yolo.keyborad.model.vo.character.KeyboardUserCharacterVO">
|
||||||
<result column="character_id" jdbcType="BIGINT" property="characterId" />
|
|
||||||
<result column="deleted" jdbcType="BOOLEAN" property="deleted" />
|
<id column="id" property="id"/>
|
||||||
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
|
|
||||||
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
|
<!-- keyboard_character 表的列名,根据你的实际字段调整 -->
|
||||||
<result column="user_id" jdbcType="BIGINT" property="userId" />
|
<result column="character_name" property="characterName"/>
|
||||||
</resultMap>
|
|
||||||
<sql id="Base_Column_List">
|
<!-- 关键:ARRAY 字段明确指定 jdbcType + typeHandler -->
|
||||||
id, character_id, deleted, created_at, updated_at, user_id
|
<result column="sort"
|
||||||
</sql>
|
jdbcType="ARRAY"
|
||||||
|
typeHandler="org.apache.ibatis.type.ArrayTypeHandler"/>
|
||||||
|
<result column="emoji" property="emoji"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<update id="updateSortByIdAndUserId">
|
||||||
|
UPDATE keyboard_user_character
|
||||||
|
SET sort = #{sort,jdbcType=ARRAY}
|
||||||
|
where user_id = #{userId}
|
||||||
|
AND deleted = FALSE
|
||||||
|
</update>
|
||||||
|
|
||||||
|
|
||||||
|
<select id="selectByUserId"
|
||||||
|
resultMap="KeyboardUserCharacterVOMap">
|
||||||
|
SELECT
|
||||||
|
kuc.id,
|
||||||
|
kc.character_name,
|
||||||
|
kuc.sort,
|
||||||
|
kuc.emoji
|
||||||
|
FROM keyboard_user_character AS kuc
|
||||||
|
LEFT JOIN keyboard_character AS kc ON kuc.character_id = kc.id
|
||||||
|
WHERE kuc.user_id = #{loginId}
|
||||||
|
AND kuc.deleted = FALSE
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
Reference in New Issue
Block a user