From 8a97a0b054c5ae16ac47a0b6881762908dd2c413 Mon Sep 17 00:00:00 2001 From: zhouzhibo Date: Wed, 14 Oct 2026 22:12:06 +0800 Subject: [PATCH] feat: add MyBatis CRUD and database integration --- AGENTS.md | 72 +++++ README.md | 18 ++ pom.xml | 9 +- .../com/hongzhen/hera/auth/AuthService.java | 30 +- .../com/hongzhen/hera/auth/LoginSession.java | 33 ++- .../hera/auth/LoginSessionMapper.java | 12 + .../hera/auth/LoginSessionRepository.java | 9 - .../java/com/hongzhen/hera/auth/SmsCode.java | 36 ++- .../com/hongzhen/hera/auth/SmsCodeMapper.java | 18 ++ .../hongzhen/hera/auth/SmsCodeRepository.java | 9 - .../com/hongzhen/hera/auth/SmsService.java | 11 +- .../com/hongzhen/hera/common/BaseEntity.java | 48 ++++ .../hongzhen/hera/common/CrudController.java | 48 ++++ .../com/hongzhen/hera/common/CrudMapper.java | 17 ++ .../com/hongzhen/hera/common/CrudService.java | 50 ++++ .../hera/common/GlobalExceptionHandler.java | 7 + .../hongzhen/hera/config/HeraProperties.java | 9 + .../com/hongzhen/hera/config/WebConfig.java | 18 +- .../com/hongzhen/hera/daily/DailyService.java | 11 +- .../java/com/hongzhen/hera/data/NfcChip.java | 128 +++++++++ .../hongzhen/hera/data/NfcChipController.java | 14 + .../com/hongzhen/hera/data/NfcChipMapper.java | 26 ++ .../hongzhen/hera/data/NfcChipService.java | 12 + .../com/hongzhen/hera/data/UserAccount.java | 180 ++++++++++++ .../hera/data/UserAccountController.java | 14 + .../hongzhen/hera/data/UserAccountMapper.java | 26 ++ .../hera/data/UserAccountService.java | 12 + .../com/hongzhen/hera/data/UserProfile.java | 262 ++++++++++++++++++ .../hera/data/UserProfileController.java | 14 + .../hongzhen/hera/data/UserProfileMapper.java | 26 ++ .../hera/data/UserProfileService.java | 12 + .../hongzhen/hera/data/UserVirtualItem.java | 88 ++++++ .../hera/data/UserVirtualItemController.java | 14 + .../hera/data/UserVirtualItemMapper.java | 26 ++ .../hera/data/UserVirtualItemOffering.java | 114 ++++++++ .../UserVirtualItemOfferingController.java | 14 + .../data/UserVirtualItemOfferingMapper.java | 26 ++ .../data/UserVirtualItemOfferingService.java | 12 + .../hera/data/UserVirtualItemService.java | 12 + .../hera/data/UserVirtualItemTransfer.java | 140 ++++++++++ .../UserVirtualItemTransferController.java | 14 + .../data/UserVirtualItemTransferMapper.java | 26 ++ .../data/UserVirtualItemTransferService.java | 12 + .../com/hongzhen/hera/data/VirtualItem.java | 74 +++++ .../hera/data/VirtualItemController.java | 14 + .../hongzhen/hera/data/VirtualItemMapper.java | 26 ++ .../hera/data/VirtualItemService.java | 12 + .../hera/security/AuthInterceptor.java | 3 + .../hera/user/AvatarStorageService.java | 62 +++++ .../com/hongzhen/hera/user/ProfileDtos.java | 8 +- .../java/com/hongzhen/hera/user/User.java | 100 ++++++- .../hongzhen/hera/user/UserController.java | 14 +- .../com/hongzhen/hera/user/UserMapper.java | 16 ++ .../hongzhen/hera/user/UserRepository.java | 9 - .../com/hongzhen/hera/user/UserService.java | 19 +- src/main/resources/application.yml | 31 ++- .../resources/mapper/LoginSessionMapper.xml | 16 ++ src/main/resources/mapper/NfcChipMapper.xml | 47 ++++ src/main/resources/mapper/SmsCodeMapper.xml | 26 ++ .../resources/mapper/UserAccountMapper.xml | 53 ++++ src/main/resources/mapper/UserMapper.xml | 52 ++++ .../resources/mapper/UserProfileMapper.xml | 63 +++++ .../mapper/UserVirtualItemMapper.xml | 41 +++ .../mapper/UserVirtualItemOfferingMapper.xml | 46 +++ .../mapper/UserVirtualItemTransferMapper.xml | 49 ++++ .../resources/mapper/VirtualItemMapper.xml | 39 +++ src/main/resources/schema.sql | 190 +++++++++++++ 67 files changed, 2597 insertions(+), 102 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/main/java/com/hongzhen/hera/auth/LoginSessionMapper.java delete mode 100644 src/main/java/com/hongzhen/hera/auth/LoginSessionRepository.java create mode 100644 src/main/java/com/hongzhen/hera/auth/SmsCodeMapper.java delete mode 100644 src/main/java/com/hongzhen/hera/auth/SmsCodeRepository.java create mode 100644 src/main/java/com/hongzhen/hera/common/BaseEntity.java create mode 100644 src/main/java/com/hongzhen/hera/common/CrudController.java create mode 100644 src/main/java/com/hongzhen/hera/common/CrudMapper.java create mode 100644 src/main/java/com/hongzhen/hera/common/CrudService.java create mode 100644 src/main/java/com/hongzhen/hera/data/NfcChip.java create mode 100644 src/main/java/com/hongzhen/hera/data/NfcChipController.java create mode 100644 src/main/java/com/hongzhen/hera/data/NfcChipMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/NfcChipService.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserAccount.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserAccountController.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserAccountMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserAccountService.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserProfile.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserProfileController.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserProfileMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserProfileService.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItem.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemController.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemOffering.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingController.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingService.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemService.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemTransfer.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferController.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferService.java create mode 100644 src/main/java/com/hongzhen/hera/data/VirtualItem.java create mode 100644 src/main/java/com/hongzhen/hera/data/VirtualItemController.java create mode 100644 src/main/java/com/hongzhen/hera/data/VirtualItemMapper.java create mode 100644 src/main/java/com/hongzhen/hera/data/VirtualItemService.java create mode 100644 src/main/java/com/hongzhen/hera/user/AvatarStorageService.java create mode 100644 src/main/java/com/hongzhen/hera/user/UserMapper.java delete mode 100644 src/main/java/com/hongzhen/hera/user/UserRepository.java create mode 100644 src/main/resources/mapper/LoginSessionMapper.xml create mode 100644 src/main/resources/mapper/NfcChipMapper.xml create mode 100644 src/main/resources/mapper/SmsCodeMapper.xml create mode 100644 src/main/resources/mapper/UserAccountMapper.xml create mode 100644 src/main/resources/mapper/UserMapper.xml create mode 100644 src/main/resources/mapper/UserProfileMapper.xml create mode 100644 src/main/resources/mapper/UserVirtualItemMapper.xml create mode 100644 src/main/resources/mapper/UserVirtualItemOfferingMapper.xml create mode 100644 src/main/resources/mapper/UserVirtualItemTransferMapper.xml create mode 100644 src/main/resources/mapper/VirtualItemMapper.xml create mode 100644 src/main/resources/schema.sql diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b17eaf6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# Hera Backend Agent Guide + +本文件适用于本仓库全部目录。Codex 修改代码前必须先阅读本文件,并遵循阿里巴巴《Java 开发手册》的命名、分层、异常处理和数据库规范。 + +## 技术基线 + +- Java 8,Spring Boot 2.7.x,MyBatis 2.3.x,MySQL 8.x。 +- 构建工具为 Maven,编码统一使用 UTF-8。 +- 不得擅自升级 Java、Spring Boot 或数据库大版本。 +- 优先复用现有包结构、公共响应、异常和鉴权组件,避免引入功能重复的依赖。 + +## 修改流程 + +1. 修改前先检查 `git status`,保留用户已有的未提交改动。 +2. 阅读需求涉及的 Controller、Service、Mapper、实体和 SQL,不凭字段名猜测数据库结构。 +3. 数据库结构以 `src/main/resources/schema.sql` 和用户提供的最新 SQL 为准。 +4. 改动保持最小范围,不做与需求无关的格式化或重构。 +5. 完成后至少执行编译、`git diff --check` 和受影响功能验证。 + +## Java 编码规范 + +- 类名使用 UpperCamelCase,方法、参数和变量使用 lowerCamelCase,常量使用 UPPER_SNAKE_CASE。 +- Mapper 接口统一以 `Mapper` 结尾,Service 和 Controller 分别以 `Service`、`Controller` 结尾。 +- 包名全部小写;按 `controller/service/mapper/entity` 职责分层,禁止跨层绕过 Service 直接访问 Mapper。 +- 方法职责保持单一。复杂条件先拆分为有业务含义的方法,避免深层嵌套。 +- 禁止无含义的魔法值;有业务含义的状态值应提取为常量或枚举。 +- 字符串比较使用常量调用 `equals`,包装类型比较使用 `equals`,不得依赖 `==`。 +- 返回集合时优先返回空集合而不是 `null`。 +- 捕获异常后必须处理、转换或记录,禁止空 `catch`;不得把数据库或系统内部异常原文直接返回前端。 +- `ThreadLocal` 必须在请求结束时执行 `remove()`。 +- 日期时间使用 `java.time` API,不新增 `Date`/`Calendar` 业务代码。 +- 不写无意义注释;实体类、实体字段和公共 API 必须使用 `/** ... */` Javadoc 说明业务含义。 + +## Spring 与接口规范 + +- Controller 只负责参数接收、鉴权上下文和响应包装,业务逻辑放在 Service。 +- 写操作需要事务时在 Service 层使用 `@Transactional`。 +- 接口统一返回 `ApiResponse`,业务错误统一抛出 `BusinessException`。 +- 新增 `/api/**` 接口默认需要 JWT;公开接口必须显式加入白名单并说明原因。 +- 请求参数必须设置合理的长度、范围和分页上限,不信任客户端传入的用户 ID 或状态值。 +- 密码、密钥、身份证信息等敏感字段不得出现在日志或接口响应中。 + +## MyBatis 规范 + +- Mapper 接口只声明方法并保留 `@Mapper`;业务 SQL 统一写在 `src/main/resources/mapper/*.xml`。 +- XML 文件名与 Mapper 接口名一致,`namespace` 必须是 Mapper 接口的完整类名。 +- 多参数方法必须使用 `@Param` 明确命名。 +- 查询必须显式列出字段,禁止使用 `SELECT *`。 +- 数据库字段使用 snake_case,Java 属性使用 lowerCamelCase;依赖 MyBatis 驼峰映射或显式 `resultMap`。 +- 新增记录需要回填主键时使用 `useGeneratedKeys="true" keyProperty="id"`。 +- 更新和删除必须带明确的主键或业务条件;禁止无条件全表更新、删除。 +- 分页参数必须在 Service 层限制范围;当前单页最大值为 100。 +- 唯一约束和外键冲突转换为可理解的业务响应,不向客户端暴露 SQL。 + +## 数据库规范 + +- 表名、字段名使用小写 snake_case;主键统一命名为 `id`。 +- 表和重要字段必须有业务注释;时间字段统一使用 `created_at`、`updated_at`。 +- 线上可重复执行的初始化脚本使用 `CREATE TABLE IF NOT EXISTS`,禁止在应用启动脚本中使用 `DROP TABLE`。 +- 修改表结构时考虑已有数据兼容性;不得通过删除重建表来完成普通字段变更。 +- 账号密码只保存安全哈希;真实姓名、身份证号等敏感信息按字段约定加密或哈希后存储。 + +## 验证命令 + +本机无全局 Maven 时,使用 IntelliJ 自带 Maven 和已安装的 JDK 8: + +```bash +env JAVA_HOME='/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home' \ + '/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3/bin/mvn' test +``` + +涉及数据库 Mapper 时,还应在可用的测试数据库上至少验证对应的查询和写入链路;写入的临时数据必须在验证结束后清理。 diff --git a/README.md b/README.md index 1539ee7..ba4cc65 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Spring Boot 后端,提供微信小程序登录、手机号验证码绑定、 ```bash export WECHAT_APP_ID=wxa76a2ef9e8ba612e export WECHAT_APP_SECRET=266bb34a7186a4b835186141516efb78 +export DB_URL='jdbc:mysql://101.43.139.56:3306/hera?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true' +export DB_USERNAME=hera_user +export DB_PASSWORD='请通过部署环境配置' mvn spring-boot:run ``` @@ -19,6 +22,21 @@ mvn spring-boot:run ## 开发说明 - 当前已默认关闭微信 mock,并接入微信 `jscode2session`。 +- 数据持久层使用 MyBatis Mapper XML,首次启动会执行 `schema.sql` 创建缺失的数据表。 +- 数据库连接默认指向 `101.43.139.56:3306/hera`,生产部署建议通过 `DB_URL`、`DB_USERNAME`、`DB_PASSWORD` 覆盖。 - `WECHAT_APP_SECRET` 请优先使用服务器环境变量配置,不建议提交到公开仓库。 - `/api/auth/sms` 开发阶段返回 `mockCode`,接入短信厂商后移除返回值。 - 正式上线前需要替换 `hera.jwt-secret`、短信服务实现和协议正文。 + +## 数据表 CRUD + +以下接口均需要携带 `Authorization: Bearer `,并支持统一操作: + +- `GET /api/data/{resource}/{id}`:按主键查询 +- `GET /api/data/{resource}?offset=0&limit=20`:分页列表,`limit` 最大为 100 +- `POST /api/data/{resource}`:新增 +- `PUT /api/data/{resource}/{id}`:更新 +- `DELETE /api/data/{resource}/{id}`:删除 + +`resource` 可取 `nfc-chips`、`user-accounts`、`user-profiles`、`virtual-items`、 +`user-virtual-items`、`offerings`、`transfers`。 diff --git a/pom.xml b/pom.xml index 3ee79c5..9f7b22a 100644 --- a/pom.xml +++ b/pom.xml @@ -27,12 +27,13 @@ spring-boot-starter-validation - org.springframework.boot - spring-boot-starter-data-jpa + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.3.2 - com.h2database - h2 + com.mysql + mysql-connector-j runtime diff --git a/src/main/java/com/hongzhen/hera/auth/AuthService.java b/src/main/java/com/hongzhen/hera/auth/AuthService.java index a463652..5f8a3ee 100644 --- a/src/main/java/com/hongzhen/hera/auth/AuthService.java +++ b/src/main/java/com/hongzhen/hera/auth/AuthService.java @@ -6,7 +6,7 @@ import com.hongzhen.hera.auth.AuthDtos.WxLoginResponse; import com.hongzhen.hera.common.BusinessException; import com.hongzhen.hera.security.JwtService; import com.hongzhen.hera.user.User; -import com.hongzhen.hera.user.UserRepository; +import com.hongzhen.hera.user.UserMapper; import com.hongzhen.hera.user.UserService; import java.time.LocalDateTime; import java.util.UUID; @@ -17,23 +17,23 @@ import org.springframework.transaction.annotation.Transactional; public class AuthService { private final WechatClient wechatClient; - private final LoginSessionRepository loginSessionRepository; - private final UserRepository userRepository; + private final LoginSessionMapper loginSessionMapper; + private final UserMapper userMapper; private final SmsService smsService; private final JwtService jwtService; private final UserService userService; public AuthService( WechatClient wechatClient, - LoginSessionRepository loginSessionRepository, - UserRepository userRepository, + LoginSessionMapper loginSessionMapper, + UserMapper userMapper, SmsService smsService, JwtService jwtService, UserService userService ) { this.wechatClient = wechatClient; - this.loginSessionRepository = loginSessionRepository; - this.userRepository = userRepository; + this.loginSessionMapper = loginSessionMapper; + this.userMapper = userMapper; this.smsService = smsService; this.jwtService = jwtService; this.userService = userService; @@ -48,25 +48,31 @@ public class AuthService { loginSession.setOpenId(wxSession.getOpenId()); loginSession.setUnionId(wxSession.getUnionId()); loginSession.setExpiresAt(LocalDateTime.now().plusMinutes(15)); - loginSessionRepository.save(loginSession); - boolean phoneBound = userRepository.findByOpenId(wxSession.getOpenId()).map(user -> user.getPhone() != null).orElse(false); + loginSessionMapper.insert(loginSession); + boolean phoneBound = userMapper.findByOpenId(wxSession.getOpenId()) + .map(user -> user.getPhone() != null) + .orElse(false); return new WxLoginResponse(sessionKey, phoneBound); } @Transactional public LoginResponse bindPhone(BindPhoneRequest request) { smsService.verify(request.getPhone(), request.getCaptcha()); - LoginSession session = loginSessionRepository.findBySessionKey(request.getSessionKey()) + LoginSession session = loginSessionMapper.findBySessionKey(request.getSessionKey()) .orElseThrow(() -> new BusinessException("登录会话不存在")); if (session.getExpiresAt().isBefore(LocalDateTime.now())) { throw new BusinessException("登录会话已过期"); } - User user = userRepository.findByOpenId(session.getOpenId()).orElseGet(User::new); + User user = userMapper.findByOpenId(session.getOpenId()).orElseGet(User::new); user.setOpenId(session.getOpenId()); user.setUnionId(session.getUnionId()); user.setPhone(request.getPhone()); user.touch(); - userRepository.save(user); + if (user.getId() == null) { + userMapper.insert(user); + } else { + userMapper.update(user); + } return new LoginResponse(jwtService.issue(user.getId()), userService.toProfile(user)); } } diff --git a/src/main/java/com/hongzhen/hera/auth/LoginSession.java b/src/main/java/com/hongzhen/hera/auth/LoginSession.java index 6cdff93..36cd69c 100644 --- a/src/main/java/com/hongzhen/hera/auth/LoginSession.java +++ b/src/main/java/com/hongzhen/hera/auth/LoginSession.java @@ -1,32 +1,45 @@ package com.hongzhen.hera.auth; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; import java.time.LocalDateTime; -@Entity +/** + * 微信登录临时会话实体。 + */ public class LoginSession { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + /** + * 主键。 + */ private Long id; - @Column(nullable = false, unique = true) + /** + * 业务登录会话键。 + */ private String sessionKey; - @Column(nullable = false) + /** + * 微信 OpenID。 + */ private String openId; + /** + * 微信 UnionID。 + */ private String unionId; + + /** + * 会话过期时间。 + */ private LocalDateTime expiresAt; public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public String getSessionKey() { return sessionKey; } diff --git a/src/main/java/com/hongzhen/hera/auth/LoginSessionMapper.java b/src/main/java/com/hongzhen/hera/auth/LoginSessionMapper.java new file mode 100644 index 0000000..47fbb03 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/auth/LoginSessionMapper.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.auth; + +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LoginSessionMapper { + + int insert(LoginSession loginSession); + + Optional findBySessionKey(String sessionKey); +} diff --git a/src/main/java/com/hongzhen/hera/auth/LoginSessionRepository.java b/src/main/java/com/hongzhen/hera/auth/LoginSessionRepository.java deleted file mode 100644 index d6f507c..0000000 --- a/src/main/java/com/hongzhen/hera/auth/LoginSessionRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.hongzhen.hera.auth; - -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface LoginSessionRepository extends JpaRepository { - - Optional findBySessionKey(String sessionKey); -} diff --git a/src/main/java/com/hongzhen/hera/auth/SmsCode.java b/src/main/java/com/hongzhen/hera/auth/SmsCode.java index 52a9981..ebcc9b9 100644 --- a/src/main/java/com/hongzhen/hera/auth/SmsCode.java +++ b/src/main/java/com/hongzhen/hera/auth/SmsCode.java @@ -1,23 +1,45 @@ package com.hongzhen.hera.auth; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; import java.time.LocalDateTime; -@Entity +/** + * 手机短信验证码实体。 + */ public class SmsCode { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + /** + * 主键。 + */ private Long id; + /** + * 接收验证码的手机号。 + */ private String phone; + + /** + * 验证码。 + */ private String code; + + /** + * 验证码过期时间。 + */ private LocalDateTime expiresAt; + + /** + * 是否已经使用。 + */ private boolean used; + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + public String getPhone() { return phone; } diff --git a/src/main/java/com/hongzhen/hera/auth/SmsCodeMapper.java b/src/main/java/com/hongzhen/hera/auth/SmsCodeMapper.java new file mode 100644 index 0000000..30a222e --- /dev/null +++ b/src/main/java/com/hongzhen/hera/auth/SmsCodeMapper.java @@ -0,0 +1,18 @@ +package com.hongzhen.hera.auth; + +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface SmsCodeMapper { + + int insert(SmsCode smsCode); + + Optional findLatestUnused( + @Param("phone") String phone, + @Param("code") String code + ); + + int markUsed(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/auth/SmsCodeRepository.java b/src/main/java/com/hongzhen/hera/auth/SmsCodeRepository.java deleted file mode 100644 index 27e75e0..0000000 --- a/src/main/java/com/hongzhen/hera/auth/SmsCodeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.hongzhen.hera.auth; - -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SmsCodeRepository extends JpaRepository { - - Optional findTopByPhoneAndCodeAndUsedFalseOrderByExpiresAtDesc(String phone, String code); -} diff --git a/src/main/java/com/hongzhen/hera/auth/SmsService.java b/src/main/java/com/hongzhen/hera/auth/SmsService.java index 76d2607..1b9e6e8 100644 --- a/src/main/java/com/hongzhen/hera/auth/SmsService.java +++ b/src/main/java/com/hongzhen/hera/auth/SmsService.java @@ -9,11 +9,11 @@ import org.springframework.transaction.annotation.Transactional; @Service public class SmsService { - private final SmsCodeRepository smsCodeRepository; + private final SmsCodeMapper smsCodeMapper; private final Random random = new Random(); - public SmsService(SmsCodeRepository smsCodeRepository) { - this.smsCodeRepository = smsCodeRepository; + public SmsService(SmsCodeMapper smsCodeMapper) { + this.smsCodeMapper = smsCodeMapper; } @Transactional @@ -23,17 +23,18 @@ public class SmsService { smsCode.setPhone(phone); smsCode.setCode(code); smsCode.setExpiresAt(LocalDateTime.now().plusMinutes(5)); - smsCodeRepository.save(smsCode); + smsCodeMapper.insert(smsCode); return code; } @Transactional public void verify(String phone, String code) { - SmsCode smsCode = smsCodeRepository.findTopByPhoneAndCodeAndUsedFalseOrderByExpiresAtDesc(phone, code) + SmsCode smsCode = smsCodeMapper.findLatestUnused(phone, code) .orElseThrow(() -> new BusinessException("验证码不正确")); if (smsCode.getExpiresAt().isBefore(LocalDateTime.now())) { throw new BusinessException("验证码已过期"); } smsCode.setUsed(true); + smsCodeMapper.markUsed(smsCode.getId()); } } diff --git a/src/main/java/com/hongzhen/hera/common/BaseEntity.java b/src/main/java/com/hongzhen/hera/common/BaseEntity.java new file mode 100644 index 0000000..ad224e0 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/common/BaseEntity.java @@ -0,0 +1,48 @@ +package com.hongzhen.hera.common; + +import java.time.LocalDateTime; + +/** + * 具有主键和审计时间的实体基类。 + */ +public abstract class BaseEntity { + + /** + * 主键。 + */ + private Long id; + + /** + * 创建时间。 + */ + private LocalDateTime createdAt; + + /** + * 最后更新时间。 + */ + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/hongzhen/hera/common/CrudController.java b/src/main/java/com/hongzhen/hera/common/CrudController.java new file mode 100644 index 0000000..03aa4d1 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/common/CrudController.java @@ -0,0 +1,48 @@ +package com.hongzhen.hera.common; + +import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +public abstract class CrudController { + + private final CrudService service; + + protected CrudController(CrudService service) { + this.service = service; + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @GetMapping + public ApiResponse> list( + @RequestParam(defaultValue = "0") int offset, + @RequestParam(defaultValue = "20") int limit + ) { + return ApiResponse.ok(service.list(offset, limit)); + } + + @PostMapping + public ApiResponse create(@RequestBody T entity) { + return ApiResponse.ok(service.create(entity)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody T entity) { + return ApiResponse.ok(service.update(id, entity)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok(null); + } +} diff --git a/src/main/java/com/hongzhen/hera/common/CrudMapper.java b/src/main/java/com/hongzhen/hera/common/CrudMapper.java new file mode 100644 index 0000000..5694e36 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/common/CrudMapper.java @@ -0,0 +1,17 @@ +package com.hongzhen.hera.common; + +import java.util.List; +import java.util.Optional; + +public interface CrudMapper { + + Optional findById(Long id); + + List findAll(int offset, int limit); + + int insert(T entity); + + int update(T entity); + + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/common/CrudService.java b/src/main/java/com/hongzhen/hera/common/CrudService.java new file mode 100644 index 0000000..803134b --- /dev/null +++ b/src/main/java/com/hongzhen/hera/common/CrudService.java @@ -0,0 +1,50 @@ +package com.hongzhen.hera.common; + +import java.util.List; +import org.springframework.transaction.annotation.Transactional; + +public abstract class CrudService { + + private static final int MAX_PAGE_SIZE = 100; + + private final CrudMapper mapper; + private final String entityName; + + protected CrudService(CrudMapper mapper, String entityName) { + this.mapper = mapper; + this.entityName = entityName; + } + + public T get(Long id) { + return mapper.findById(id).orElseThrow(() -> new BusinessException(entityName + "不存在")); + } + + public List list(int offset, int limit) { + int safeOffset = Math.max(0, offset); + int safeLimit = Math.max(1, Math.min(limit, MAX_PAGE_SIZE)); + return mapper.findAll(safeOffset, safeLimit); + } + + @Transactional + public T create(T entity) { + mapper.insert(entity); + return get(entity.getId()); + } + + @Transactional + public T update(Long id, T entity) { + get(id); + entity.setId(id); + if (mapper.update(entity) == 0) { + throw new BusinessException(entityName + "更新失败"); + } + return get(id); + } + + @Transactional + public void delete(Long id) { + if (mapper.deleteById(id) == 0) { + throw new BusinessException(entityName + "不存在"); + } + } +} diff --git a/src/main/java/com/hongzhen/hera/common/GlobalExceptionHandler.java b/src/main/java/com/hongzhen/hera/common/GlobalExceptionHandler.java index 65eb292..69cc0f6 100644 --- a/src/main/java/com/hongzhen/hera/common/GlobalExceptionHandler.java +++ b/src/main/java/com/hongzhen/hera/common/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.hongzhen.hera.common; import org.springframework.http.HttpStatus; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -21,4 +22,10 @@ public class GlobalExceptionHandler { public ApiResponse handleValidation(Exception exception) { return ApiResponse.fail("参数不正确"); } + + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiResponse handleDataIntegrity(DataIntegrityViolationException exception) { + return ApiResponse.fail("数据重复或关联关系不正确"); + } } diff --git a/src/main/java/com/hongzhen/hera/config/HeraProperties.java b/src/main/java/com/hongzhen/hera/config/HeraProperties.java index efff732..9d20f9f 100644 --- a/src/main/java/com/hongzhen/hera/config/HeraProperties.java +++ b/src/main/java/com/hongzhen/hera/config/HeraProperties.java @@ -6,6 +6,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public class HeraProperties { private String jwtSecret; + private String uploadDir = "./uploads"; private final Wx wx = new Wx(); private final Sms sms = new Sms(); @@ -17,6 +18,14 @@ public class HeraProperties { this.jwtSecret = jwtSecret; } + public String getUploadDir() { + return uploadDir; + } + + public void setUploadDir(String uploadDir) { + this.uploadDir = uploadDir; + } + public Wx getWx() { return wx; } diff --git a/src/main/java/com/hongzhen/hera/config/WebConfig.java b/src/main/java/com/hongzhen/hera/config/WebConfig.java index 7756013..acced20 100644 --- a/src/main/java/com/hongzhen/hera/config/WebConfig.java +++ b/src/main/java/com/hongzhen/hera/config/WebConfig.java @@ -5,16 +5,21 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.nio.file.Paths; + @Configuration @EnableConfigurationProperties(HeraProperties.class) public class WebConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; + private final HeraProperties properties; - public WebConfig(AuthInterceptor authInterceptor) { + public WebConfig(AuthInterceptor authInterceptor, HeraProperties properties) { this.authInterceptor = authInterceptor; + this.properties = properties; } @Override @@ -26,6 +31,15 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") - .excludePathPatterns("/api/auth/**", "/api/content/**", "/h2-console/**"); + .excludePathPatterns("/api/auth/**", "/api/content/**"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + String uploadLocation = Paths.get(properties.getUploadDir()).toAbsolutePath().normalize().toUri().toString(); + if (!uploadLocation.endsWith("/")) { + uploadLocation += "/"; + } + registry.addResourceHandler("/uploads/**").addResourceLocations(uploadLocation); } } diff --git a/src/main/java/com/hongzhen/hera/daily/DailyService.java b/src/main/java/com/hongzhen/hera/daily/DailyService.java index 41359c1..2bcc663 100644 --- a/src/main/java/com/hongzhen/hera/daily/DailyService.java +++ b/src/main/java/com/hongzhen/hera/daily/DailyService.java @@ -7,7 +7,7 @@ import com.hongzhen.hera.daily.DailyDtos.FortuneCard; import com.hongzhen.hera.daily.DailyDtos.LifeTip; import com.hongzhen.hera.daily.DailyDtos.LuckyPanel; import com.hongzhen.hera.user.User; -import com.hongzhen.hera.user.UserRepository; +import com.hongzhen.hera.user.UserMapper; import com.nlf.calendar.Solar; import java.time.DayOfWeek; import java.time.LocalDate; @@ -19,10 +19,10 @@ import org.springframework.transaction.annotation.Transactional; @Service public class DailyService { - private final UserRepository userRepository; + private final UserMapper userMapper; - public DailyService(UserRepository userRepository) { - this.userRepository = userRepository; + public DailyService(UserMapper userMapper) { + this.userMapper = userMapper; } public DailyHomeResponse home(Long userId) { @@ -59,11 +59,12 @@ public class DailyService { String nextPinnedCardId = cardId.equals(user.getPinnedDailyCardId()) ? null : cardId; user.setPinnedDailyCardId(nextPinnedCardId); user.touch(); + userMapper.update(user); return nextPinnedCardId; } private User user(Long userId) { - return userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在")); + return userMapper.findById(userId).orElseThrow(() -> new BusinessException("用户不存在")); } private String lunarDate(LocalDate date) { diff --git a/src/main/java/com/hongzhen/hera/data/NfcChip.java b/src/main/java/com/hongzhen/hera/data/NfcChip.java new file mode 100644 index 0000000..db95ddd --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/NfcChip.java @@ -0,0 +1,128 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.BaseEntity; + +import java.time.LocalDateTime; + +/** + * NFC 芯片实体。 + */ +public class NfcChip extends BaseEntity { + + /** + * 芯片唯一标识。 + */ + private String uid; + + /** + * 芯片型号。 + */ + private String chipType; + + /** + * 芯片制造商。 + */ + private String manufacturer; + + /** + * 用户可写容量,单位为字节。 + */ + private Integer capacity; + + /** + * NDEF 内容,格式为 JSON 或文本。 + */ + private String ndefContent; + + /** + * 手机支架产品类型。 + */ + private Integer holderType; + + /** + * 预置虚拟物 ID。 + */ + private Integer virtualItemId; + + /** + * 激活时间。 + */ + private LocalDateTime activatedAt; + + /** + * 最后登录时间。 + */ + private LocalDateTime lastLoginAt; + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getChipType() { + return chipType; + } + + public void setChipType(String chipType) { + this.chipType = chipType; + } + + public String getManufacturer() { + return manufacturer; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + public Integer getCapacity() { + return capacity; + } + + public void setCapacity(Integer capacity) { + this.capacity = capacity; + } + + public String getNdefContent() { + return ndefContent; + } + + public void setNdefContent(String ndefContent) { + this.ndefContent = ndefContent; + } + + public Integer getHolderType() { + return holderType; + } + + public void setHolderType(Integer holderType) { + this.holderType = holderType; + } + + public Integer getVirtualItemId() { + return virtualItemId; + } + + public void setVirtualItemId(Integer virtualItemId) { + this.virtualItemId = virtualItemId; + } + + public LocalDateTime getActivatedAt() { + return activatedAt; + } + + public void setActivatedAt(LocalDateTime activatedAt) { + this.activatedAt = activatedAt; + } + + public LocalDateTime getLastLoginAt() { + return lastLoginAt; + } + + public void setLastLoginAt(LocalDateTime lastLoginAt) { + this.lastLoginAt = lastLoginAt; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/NfcChipController.java b/src/main/java/com/hongzhen/hera/data/NfcChipController.java new file mode 100644 index 0000000..380d900 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/NfcChipController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/nfc-chips") +public class NfcChipController extends CrudController { + + public NfcChipController(NfcChipService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/NfcChipMapper.java b/src/main/java/com/hongzhen/hera/data/NfcChipMapper.java new file mode 100644 index 0000000..b06677d --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/NfcChipMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface NfcChipMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(NfcChip entity); + + @Override + int update(NfcChip entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/NfcChipService.java b/src/main/java/com/hongzhen/hera/data/NfcChipService.java new file mode 100644 index 0000000..084cadb --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/NfcChipService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class NfcChipService extends CrudService { + + public NfcChipService(NfcChipMapper mapper) { + super(mapper, "NFC芯片"); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserAccount.java b/src/main/java/com/hongzhen/hera/data/UserAccount.java new file mode 100644 index 0000000..c4ca911 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserAccount.java @@ -0,0 +1,180 @@ +package com.hongzhen.hera.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hongzhen.hera.common.BaseEntity; + +/** + * 用户账号实体。 + */ +public class UserAccount extends BaseEntity { + + /** + * 登录用户名。 + */ + private String username; + + /** + * 用户显示昵称。 + */ + private String nickname; + + /** + * 登录密码哈希。 + */ + private String passwordHash; + + /** + * 手机号。 + */ + private String mobile; + + /** + * 电子邮箱。 + */ + private String email; + + /** + * 已绑定的 NFC 标签 UID。 + */ + private String nfcUid; + + /** + * 微信 OpenID。 + */ + private String wechatOpenid; + + /** + * 微信 UnionID。 + */ + private String wechatUnionid; + + /** + * 账号状态:1 正常,0 禁用。 + */ + private Integer status; + + /** + * 注册国家代码。 + */ + private String registerCountryCode; + + /** + * 注册地区代码。 + */ + private String registerRegionCode; + + /** + * 注册城市。 + */ + private String registerCity; + + /** + * 注册时区。 + */ + private String registerTimezone; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getNfcUid() { + return nfcUid; + } + + public void setNfcUid(String nfcUid) { + this.nfcUid = nfcUid; + } + + public String getWechatOpenid() { + return wechatOpenid; + } + + public void setWechatOpenid(String wechatOpenid) { + this.wechatOpenid = wechatOpenid; + } + + public String getWechatUnionid() { + return wechatUnionid; + } + + public void setWechatUnionid(String wechatUnionid) { + this.wechatUnionid = wechatUnionid; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getRegisterCountryCode() { + return registerCountryCode; + } + + public void setRegisterCountryCode(String registerCountryCode) { + this.registerCountryCode = registerCountryCode; + } + + public String getRegisterRegionCode() { + return registerRegionCode; + } + + public void setRegisterRegionCode(String registerRegionCode) { + this.registerRegionCode = registerRegionCode; + } + + public String getRegisterCity() { + return registerCity; + } + + public void setRegisterCity(String registerCity) { + this.registerCity = registerCity; + } + + public String getRegisterTimezone() { + return registerTimezone; + } + + public void setRegisterTimezone(String registerTimezone) { + this.registerTimezone = registerTimezone; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserAccountController.java b/src/main/java/com/hongzhen/hera/data/UserAccountController.java new file mode 100644 index 0000000..734efdc --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserAccountController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/user-accounts") +public class UserAccountController extends CrudController { + + public UserAccountController(UserAccountService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserAccountMapper.java b/src/main/java/com/hongzhen/hera/data/UserAccountMapper.java new file mode 100644 index 0000000..4e77ca0 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserAccountMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserAccountMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(UserAccount entity); + + @Override + int update(UserAccount entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/UserAccountService.java b/src/main/java/com/hongzhen/hera/data/UserAccountService.java new file mode 100644 index 0000000..0819c1b --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserAccountService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class UserAccountService extends CrudService { + + public UserAccountService(UserAccountMapper mapper) { + super(mapper, "用户账号"); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserProfile.java b/src/main/java/com/hongzhen/hera/data/UserProfile.java new file mode 100644 index 0000000..7c79d94 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserProfile.java @@ -0,0 +1,262 @@ +package com.hongzhen.hera.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hongzhen.hera.common.BaseEntity; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 用户资料实体。 + */ +public class UserProfile extends BaseEntity { + + /** + * 关联的用户账号 ID。 + */ + private Long userId; + + /** + * 加密存储的真实姓名。 + */ + private String realNameEncrypted; + + /** + * 性别:0 未知,1 男,2 女。 + */ + private Integer gender; + + /** + * 出生日期。 + */ + private LocalDate birthday; + + /** + * 出生时辰,取值范围为 0 至 11。 + */ + private Integer birthShichen; + + /** + * 出生国家代码。 + */ + private String birthCountryCode; + + /** + * 出生地区代码。 + */ + private String birthRegionCode; + + /** + * 出生城市代码。 + */ + private String birthCityCode; + + /** + * 出生区县代码。 + */ + private String birthDistrictCode; + + /** + * 加密存储的身份证号。 + */ + private String idCardNoEncrypted; + + /** + * 身份证号 SHA-256 哈希。 + */ + private String idCardHash; + + /** + * 实名认证状态:0 未认证,1 已认证。 + */ + private Integer idVerified; + + /** + * 实名认证时间。 + */ + private LocalDateTime idVerifiedAt; + + /** + * 头像 URL。 + */ + private String avatarUrl; + + /** + * 现居住省份。 + */ + private String residenceProvince; + + /** + * 现居住城市。 + */ + private String residenceCity; + + /** + * 现居住区县。 + */ + private String residenceDistrict; + + /** + * 现居住详细地址。 + */ + private String residenceAddress; + + /** + * 个性签名。 + */ + private String signature; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String getRealNameEncrypted() { + return realNameEncrypted; + } + + public void setRealNameEncrypted(String realNameEncrypted) { + this.realNameEncrypted = realNameEncrypted; + } + + public Integer getGender() { + return gender; + } + + public void setGender(Integer gender) { + this.gender = gender; + } + + public LocalDate getBirthday() { + return birthday; + } + + public void setBirthday(LocalDate birthday) { + this.birthday = birthday; + } + + public Integer getBirthShichen() { + return birthShichen; + } + + public void setBirthShichen(Integer birthShichen) { + this.birthShichen = birthShichen; + } + + public String getBirthCountryCode() { + return birthCountryCode; + } + + public void setBirthCountryCode(String birthCountryCode) { + this.birthCountryCode = birthCountryCode; + } + + public String getBirthRegionCode() { + return birthRegionCode; + } + + public void setBirthRegionCode(String birthRegionCode) { + this.birthRegionCode = birthRegionCode; + } + + public String getBirthCityCode() { + return birthCityCode; + } + + public void setBirthCityCode(String birthCityCode) { + this.birthCityCode = birthCityCode; + } + + public String getBirthDistrictCode() { + return birthDistrictCode; + } + + public void setBirthDistrictCode(String birthDistrictCode) { + this.birthDistrictCode = birthDistrictCode; + } + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String getIdCardNoEncrypted() { + return idCardNoEncrypted; + } + + public void setIdCardNoEncrypted(String idCardNoEncrypted) { + this.idCardNoEncrypted = idCardNoEncrypted; + } + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String getIdCardHash() { + return idCardHash; + } + + public void setIdCardHash(String idCardHash) { + this.idCardHash = idCardHash; + } + + public Integer getIdVerified() { + return idVerified; + } + + public void setIdVerified(Integer idVerified) { + this.idVerified = idVerified; + } + + public LocalDateTime getIdVerifiedAt() { + return idVerifiedAt; + } + + public void setIdVerifiedAt(LocalDateTime idVerifiedAt) { + this.idVerifiedAt = idVerifiedAt; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getResidenceProvince() { + return residenceProvince; + } + + public void setResidenceProvince(String residenceProvince) { + this.residenceProvince = residenceProvince; + } + + public String getResidenceCity() { + return residenceCity; + } + + public void setResidenceCity(String residenceCity) { + this.residenceCity = residenceCity; + } + + public String getResidenceDistrict() { + return residenceDistrict; + } + + public void setResidenceDistrict(String residenceDistrict) { + this.residenceDistrict = residenceDistrict; + } + + public String getResidenceAddress() { + return residenceAddress; + } + + public void setResidenceAddress(String residenceAddress) { + this.residenceAddress = residenceAddress; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserProfileController.java b/src/main/java/com/hongzhen/hera/data/UserProfileController.java new file mode 100644 index 0000000..c89fff9 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserProfileController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/user-profiles") +public class UserProfileController extends CrudController { + + public UserProfileController(UserProfileService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserProfileMapper.java b/src/main/java/com/hongzhen/hera/data/UserProfileMapper.java new file mode 100644 index 0000000..d3c10d3 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserProfileMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserProfileMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(UserProfile entity); + + @Override + int update(UserProfile entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/UserProfileService.java b/src/main/java/com/hongzhen/hera/data/UserProfileService.java new file mode 100644 index 0000000..d9e72df --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserProfileService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class UserProfileService extends CrudService { + + public UserProfileService(UserProfileMapper mapper) { + super(mapper, "用户资料"); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItem.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItem.java new file mode 100644 index 0000000..4a3e8dc --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItem.java @@ -0,0 +1,88 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.BaseEntity; +import java.time.LocalDateTime; + +/** + * 用户持有虚拟物关系实体。 + */ +public class UserVirtualItem extends BaseEntity { + + /** + * 用户账号 ID。 + */ + private Long userId; + + /** + * 虚拟物 ID。 + */ + private Integer virtualItemId; + + /** + * 来源 NFC 芯片 UID。 + */ + private String nfcUid; + + /** + * 用户自定义顺序,值越小越靠前。 + */ + private Integer sortOrder; + + /** + * 持有状态:1 供奉中,0 已送神,2 已回向,3 已过期。 + */ + private Integer status; + + /** + * 获得时间。 + */ + private LocalDateTime obtainedAt; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Integer getVirtualItemId() { + return virtualItemId; + } + + public void setVirtualItemId(Integer virtualItemId) { + this.virtualItemId = virtualItemId; + } + + public String getNfcUid() { + return nfcUid; + } + + public void setNfcUid(String nfcUid) { + this.nfcUid = nfcUid; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public LocalDateTime getObtainedAt() { + return obtainedAt; + } + + public void setObtainedAt(LocalDateTime obtainedAt) { + this.obtainedAt = obtainedAt; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemController.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemController.java new file mode 100644 index 0000000..8c540b6 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/user-virtual-items") +public class UserVirtualItemController extends CrudController { + + public UserVirtualItemController(UserVirtualItemService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemMapper.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemMapper.java new file mode 100644 index 0000000..1e04e65 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserVirtualItemMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(UserVirtualItem entity); + + @Override + int update(UserVirtualItem entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemOffering.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOffering.java new file mode 100644 index 0000000..80a1ad1 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOffering.java @@ -0,0 +1,114 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.BaseEntity; +import java.time.LocalDateTime; + +/** + * 用户虚拟物供奉记录实体。 + */ +public class UserVirtualItemOffering extends BaseEntity { + + /** + * 用户虚拟物关系 ID。 + */ + private Long userVirtualItemId; + + /** + * 用户账号 ID。 + */ + private Long userId; + + /** + * 虚拟物 ID。 + */ + private Integer virtualItemId; + + /** + * 供奉类型:1 请神,2 上香,3 祈愿,4 功德,5 送神。 + */ + private Integer offeringType; + + /** + * 祈愿文字或功德备注。 + */ + private String content; + + /** + * 功德值。 + */ + private Integer meritValue; + + /** + * 记录状态:1 生效,0 失效。 + */ + private Integer status; + + /** + * 供奉时间。 + */ + private LocalDateTime offeredAt; + + public Long getUserVirtualItemId() { + return userVirtualItemId; + } + + public void setUserVirtualItemId(Long userVirtualItemId) { + this.userVirtualItemId = userVirtualItemId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Integer getVirtualItemId() { + return virtualItemId; + } + + public void setVirtualItemId(Integer virtualItemId) { + this.virtualItemId = virtualItemId; + } + + public Integer getOfferingType() { + return offeringType; + } + + public void setOfferingType(Integer offeringType) { + this.offeringType = offeringType; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Integer getMeritValue() { + return meritValue; + } + + public void setMeritValue(Integer meritValue) { + this.meritValue = meritValue; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public LocalDateTime getOfferedAt() { + return offeredAt; + } + + public void setOfferedAt(LocalDateTime offeredAt) { + this.offeredAt = offeredAt; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingController.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingController.java new file mode 100644 index 0000000..d1a0134 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/offerings") +public class UserVirtualItemOfferingController extends CrudController { + + public UserVirtualItemOfferingController(UserVirtualItemOfferingService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingMapper.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingMapper.java new file mode 100644 index 0000000..5435fab --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserVirtualItemOfferingMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(UserVirtualItemOffering entity); + + @Override + int update(UserVirtualItemOffering entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingService.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingService.java new file mode 100644 index 0000000..ce2a21b --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemOfferingService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class UserVirtualItemOfferingService extends CrudService { + + public UserVirtualItemOfferingService(UserVirtualItemOfferingMapper mapper) { + super(mapper, "供奉记录"); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemService.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemService.java new file mode 100644 index 0000000..f0db76b --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class UserVirtualItemService extends CrudService { + + public UserVirtualItemService(UserVirtualItemMapper mapper) { + super(mapper, "用户虚拟物"); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransfer.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransfer.java new file mode 100644 index 0000000..502db76 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransfer.java @@ -0,0 +1,140 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.BaseEntity; +import java.time.LocalDateTime; + +/** + * 用户虚拟物回向记录实体。 + */ +public class UserVirtualItemTransfer extends BaseEntity { + + /** + * 转出用户 ID。 + */ + private Long fromUserId; + + /** + * 转出用户昵称快照。 + */ + private String fromUserName; + + /** + * 接收用户 ID。 + */ + private Long toUserId; + + /** + * 接收用户昵称快照。 + */ + private String toUserName; + + /** + * 虚拟物 ID。 + */ + private Integer virtualItemId; + + /** + * 转出方原持有关系 ID。 + */ + private Long fromUserItemId; + + /** + * 接收方新持有关系 ID。 + */ + private Long toUserItemId; + + /** + * 回向寄语或祝福语。 + */ + private String content; + + /** + * 记录状态:1 生效,0 已撤销。 + */ + private Integer status; + + /** + * 回向时间。 + */ + private LocalDateTime transferredAt; + + public Long getFromUserId() { + return fromUserId; + } + + public void setFromUserId(Long fromUserId) { + this.fromUserId = fromUserId; + } + + public String getFromUserName() { + return fromUserName; + } + + public void setFromUserName(String fromUserName) { + this.fromUserName = fromUserName; + } + + public Long getToUserId() { + return toUserId; + } + + public void setToUserId(Long toUserId) { + this.toUserId = toUserId; + } + + public String getToUserName() { + return toUserName; + } + + public void setToUserName(String toUserName) { + this.toUserName = toUserName; + } + + public Integer getVirtualItemId() { + return virtualItemId; + } + + public void setVirtualItemId(Integer virtualItemId) { + this.virtualItemId = virtualItemId; + } + + public Long getFromUserItemId() { + return fromUserItemId; + } + + public void setFromUserItemId(Long fromUserItemId) { + this.fromUserItemId = fromUserItemId; + } + + public Long getToUserItemId() { + return toUserItemId; + } + + public void setToUserItemId(Long toUserItemId) { + this.toUserItemId = toUserItemId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public LocalDateTime getTransferredAt() { + return transferredAt; + } + + public void setTransferredAt(LocalDateTime transferredAt) { + this.transferredAt = transferredAt; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferController.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferController.java new file mode 100644 index 0000000..aeb7903 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/transfers") +public class UserVirtualItemTransferController extends CrudController { + + public UserVirtualItemTransferController(UserVirtualItemTransferService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferMapper.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferMapper.java new file mode 100644 index 0000000..b75f623 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserVirtualItemTransferMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(UserVirtualItemTransfer entity); + + @Override + int update(UserVirtualItemTransfer entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferService.java b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferService.java new file mode 100644 index 0000000..a87ac68 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/UserVirtualItemTransferService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class UserVirtualItemTransferService extends CrudService { + + public UserVirtualItemTransferService(UserVirtualItemTransferMapper mapper) { + super(mapper, "回向记录"); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/VirtualItem.java b/src/main/java/com/hongzhen/hera/data/VirtualItem.java new file mode 100644 index 0000000..9042949 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/VirtualItem.java @@ -0,0 +1,74 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.BaseEntity; + +/** + * 虚拟物字典实体。 + */ +public class VirtualItem extends BaseEntity { + + /** + * 虚拟物名称。 + */ + private String itemName; + + /** + * 图标 URL。 + */ + private String iconUrl; + + /** + * 虚拟物描述。 + */ + private String description; + + /** + * 展示顺序,值越小越靠前。 + */ + private Integer sortOrder; + + /** + * 启用状态:1 启用,0 禁用。 + */ + private Integer enabled; + + public String getItemName() { + return itemName; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } + + public String getIconUrl() { + return iconUrl; + } + + public void setIconUrl(String iconUrl) { + this.iconUrl = iconUrl; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public Integer getEnabled() { + return enabled; + } + + public void setEnabled(Integer enabled) { + this.enabled = enabled; + } +} diff --git a/src/main/java/com/hongzhen/hera/data/VirtualItemController.java b/src/main/java/com/hongzhen/hera/data/VirtualItemController.java new file mode 100644 index 0000000..58710e9 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/VirtualItemController.java @@ -0,0 +1,14 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/data/virtual-items") +public class VirtualItemController extends CrudController { + + public VirtualItemController(VirtualItemService service) { + super(service); + } +} diff --git a/src/main/java/com/hongzhen/hera/data/VirtualItemMapper.java b/src/main/java/com/hongzhen/hera/data/VirtualItemMapper.java new file mode 100644 index 0000000..ab95392 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/VirtualItemMapper.java @@ -0,0 +1,26 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudMapper; +import java.util.List; +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface VirtualItemMapper extends CrudMapper { + + @Override + Optional findById(Long id); + + @Override + List findAll(@Param("offset") int offset, @Param("limit") int limit); + + @Override + int insert(VirtualItem entity); + + @Override + int update(VirtualItem entity); + + @Override + int deleteById(Long id); +} diff --git a/src/main/java/com/hongzhen/hera/data/VirtualItemService.java b/src/main/java/com/hongzhen/hera/data/VirtualItemService.java new file mode 100644 index 0000000..0aa59fa --- /dev/null +++ b/src/main/java/com/hongzhen/hera/data/VirtualItemService.java @@ -0,0 +1,12 @@ +package com.hongzhen.hera.data; + +import com.hongzhen.hera.common.CrudService; +import org.springframework.stereotype.Service; + +@Service +public class VirtualItemService extends CrudService { + + public VirtualItemService(VirtualItemMapper mapper) { + super(mapper, "虚拟物"); + } +} diff --git a/src/main/java/com/hongzhen/hera/security/AuthInterceptor.java b/src/main/java/com/hongzhen/hera/security/AuthInterceptor.java index 678ed4d..cf2db9a 100644 --- a/src/main/java/com/hongzhen/hera/security/AuthInterceptor.java +++ b/src/main/java/com/hongzhen/hera/security/AuthInterceptor.java @@ -17,6 +17,9 @@ public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } String auth = request.getHeader("Authorization"); if (auth == null || !auth.startsWith("Bearer ")) { throw new BusinessException("请先登录"); diff --git a/src/main/java/com/hongzhen/hera/user/AvatarStorageService.java b/src/main/java/com/hongzhen/hera/user/AvatarStorageService.java new file mode 100644 index 0000000..23cb7e4 --- /dev/null +++ b/src/main/java/com/hongzhen/hera/user/AvatarStorageService.java @@ -0,0 +1,62 @@ +package com.hongzhen.hera.user; + +import com.hongzhen.hera.common.BusinessException; +import com.hongzhen.hera.config.HeraProperties; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Locale; +import java.util.UUID; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class AvatarStorageService { + + private static final long MAX_AVATAR_SIZE = 5L * 1024L * 1024L; + private final Path avatarDirectory; + + public AvatarStorageService(HeraProperties properties) { + this.avatarDirectory = Paths.get(properties.getUploadDir(), "avatars").toAbsolutePath().normalize(); + } + + public String store(Long userId, MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BusinessException("请选择头像图片"); + } + if (file.getSize() > MAX_AVATAR_SIZE) { + throw new BusinessException("头像图片不能超过5MB"); + } + + String contentType = file.getContentType() == null ? "" : file.getContentType().toLowerCase(Locale.ROOT); + String extension; + if ("image/jpeg".equals(contentType) || "image/jpg".equals(contentType)) { + extension = ".jpg"; + } else if ("image/png".equals(contentType)) { + extension = ".png"; + } else if ("image/webp".equals(contentType)) { + extension = ".webp"; + } else { + throw new BusinessException("仅支持 JPG、PNG 或 WebP 头像"); + } + + String filename = userId + "-" + UUID.randomUUID().toString().replace("-", "") + extension; + Path target = avatarDirectory.resolve(filename).normalize(); + if (!target.startsWith(avatarDirectory)) { + throw new BusinessException("头像文件名不合法"); + } + + try { + Files.createDirectories(avatarDirectory); + try (InputStream input = file.getInputStream()) { + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException exception) { + throw new BusinessException("头像保存失败,请稍后重试"); + } + return "/uploads/avatars/" + filename; + } +} diff --git a/src/main/java/com/hongzhen/hera/user/ProfileDtos.java b/src/main/java/com/hongzhen/hera/user/ProfileDtos.java index b0a5048..6d8a070 100644 --- a/src/main/java/com/hongzhen/hera/user/ProfileDtos.java +++ b/src/main/java/com/hongzhen/hera/user/ProfileDtos.java @@ -11,6 +11,7 @@ public class ProfileDtos { private Long id; private String nickname; private String phone; + private String avatarUrl; private String gender; private LocalDate birthday; private String birthHour; @@ -21,11 +22,12 @@ public class ProfileDtos { private int remainingEdits; private ElementReport elementReport; - public ProfileResponse(Long id, String nickname, String phone, String gender, LocalDate birthday, String birthHour, + public ProfileResponse(Long id, String nickname, String phone, String avatarUrl, String gender, LocalDate birthday, String birthHour, String province, String city, String district, int points, int remainingEdits, ElementReport elementReport) { this.id = id; this.nickname = nickname; this.phone = phone; + this.avatarUrl = avatarUrl; this.gender = gender; this.birthday = birthday; this.birthHour = birthHour; @@ -49,6 +51,10 @@ public class ProfileDtos { return phone; } + public String getAvatarUrl() { + return avatarUrl; + } + public String getGender() { return gender; } diff --git a/src/main/java/com/hongzhen/hera/user/User.java b/src/main/java/com/hongzhen/hera/user/User.java index 0342d77..04e5134 100644 --- a/src/main/java/com/hongzhen/hera/user/User.java +++ b/src/main/java/com/hongzhen/hera/user/User.java @@ -1,44 +1,106 @@ package com.hongzhen.hera.user; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; import java.time.LocalDate; import java.time.LocalDateTime; -@Entity -@Table(name = "hera_user") +/** + * 小程序业务用户实体。 + */ public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + /** + * 主键。 + */ private Long id; - @Column(nullable = false, unique = true) + /** + * 微信 OpenID。 + */ private String openId; + /** + * 微信 UnionID。 + */ private String unionId; + + /** + * 已绑定手机号。 + */ private String phone; + + /** + * 头像 URL。 + */ + private String avatarUrl; + + /** + * 用户昵称。 + */ private String nickname = "道道3338"; + + /** + * 用户性别。 + */ private String gender = "男"; + + /** + * 出生日期。 + */ private LocalDate birthday = LocalDate.of(2008, 12, 28); + + /** + * 出生时辰。 + */ private String birthHour = "寅时"; + + /** + * 所在省份。 + */ private String province = "河南省"; + + /** + * 所在城市。 + */ private String city = "洛阳市"; + + /** + * 所在区县。 + */ private String district = "老城区"; + + /** + * 当前吉分。 + */ private int points = 0; + + /** + * 已置顶的每日卡片 ID。 + */ private String pinnedDailyCardId; + + /** + * 当前周期内资料修改次数。 + */ private int profileEditCount = 0; + + /** + * 创建时间。 + */ private LocalDateTime createdAt = LocalDateTime.now(); + + /** + * 最后更新时间。 + */ private LocalDateTime updatedAt = LocalDateTime.now(); public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public String getOpenId() { return openId; } @@ -63,6 +125,14 @@ public class User { this.phone = phone; } + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + public String getNickname() { return nickname; } @@ -147,10 +217,18 @@ public class User { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + public void touch() { updatedAt = LocalDateTime.now(); } diff --git a/src/main/java/com/hongzhen/hera/user/UserController.java b/src/main/java/com/hongzhen/hera/user/UserController.java index 646423a..8a6a3da 100644 --- a/src/main/java/com/hongzhen/hera/user/UserController.java +++ b/src/main/java/com/hongzhen/hera/user/UserController.java @@ -5,20 +5,26 @@ import com.hongzhen.hera.security.CurrentUser; import com.hongzhen.hera.user.ProfileDtos.ProfileResponse; import com.hongzhen.hera.user.ProfileDtos.UpdateProfileRequest; import javax.validation.Valid; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/user") public class UserController { private final UserService userService; + private final AvatarStorageService avatarStorageService; - public UserController(UserService userService) { + public UserController(UserService userService, AvatarStorageService avatarStorageService) { this.userService = userService; + this.avatarStorageService = avatarStorageService; } @GetMapping("/profile") @@ -30,4 +36,10 @@ public class UserController { public ApiResponse update(@Valid @RequestBody UpdateProfileRequest request) { return ApiResponse.ok(userService.updateProfile(CurrentUser.id(), request)); } + + @PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ApiResponse uploadAvatar(@RequestParam("file") MultipartFile file) { + String avatarUrl = avatarStorageService.store(CurrentUser.id(), file); + return ApiResponse.ok(userService.updateAvatar(CurrentUser.id(), avatarUrl)); + } } diff --git a/src/main/java/com/hongzhen/hera/user/UserMapper.java b/src/main/java/com/hongzhen/hera/user/UserMapper.java new file mode 100644 index 0000000..16317ba --- /dev/null +++ b/src/main/java/com/hongzhen/hera/user/UserMapper.java @@ -0,0 +1,16 @@ +package com.hongzhen.hera.user; + +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper { + + Optional findById(Long id); + + Optional findByOpenId(String openId); + + int insert(User user); + + int update(User user); +} diff --git a/src/main/java/com/hongzhen/hera/user/UserRepository.java b/src/main/java/com/hongzhen/hera/user/UserRepository.java deleted file mode 100644 index 8d4cfa1..0000000 --- a/src/main/java/com/hongzhen/hera/user/UserRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.hongzhen.hera.user; - -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRepository extends JpaRepository { - - Optional findByOpenId(String openId); -} diff --git a/src/main/java/com/hongzhen/hera/user/UserService.java b/src/main/java/com/hongzhen/hera/user/UserService.java index a76f8fb..62674a0 100644 --- a/src/main/java/com/hongzhen/hera/user/UserService.java +++ b/src/main/java/com/hongzhen/hera/user/UserService.java @@ -11,16 +11,16 @@ import org.springframework.transaction.annotation.Transactional; @Service public class UserService { - private final UserRepository userRepository; + private final UserMapper userMapper; private final FortuneService fortuneService; - public UserService(UserRepository userRepository, FortuneService fortuneService) { - this.userRepository = userRepository; + public UserService(UserMapper userMapper, FortuneService fortuneService) { + this.userMapper = userMapper; this.fortuneService = fortuneService; } public User get(Long userId) { - return userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在")); + return userMapper.findById(userId).orElseThrow(() -> new BusinessException("用户不存在")); } public ProfileResponse profile(Long userId) { @@ -42,6 +42,16 @@ public class UserService { user.setDistrict(request.getDistrict()); user.setProfileEditCount(user.getProfileEditCount() + 1); user.touch(); + userMapper.update(user); + return toProfile(user); + } + + @Transactional + public ProfileResponse updateAvatar(Long userId, String avatarUrl) { + User user = get(userId); + user.setAvatarUrl(avatarUrl); + user.touch(); + userMapper.update(user); return toProfile(user); } @@ -51,6 +61,7 @@ public class UserService { user.getId(), user.getNickname(), user.getPhone(), + user.getAvatarUrl(), user.getGender(), user.getBirthday(), user.getBirthHour(), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 58edb69..04dbcde 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,22 +5,27 @@ spring: application: name: hera datasource: - url: jdbc:h2:mem:hera;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1 - username: sa - password: - h2: - console: - enabled: true - jpa: - hibernate: - ddl-auto: update - open-in-view: false - properties: - hibernate: - format_sql: true + driver-class-name: com.mysql.cj.jdbc.Driver + url: "${DB_URL:jdbc:mysql://101.43.139.56:3306/hera?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}" + username: "${DB_USERNAME:hera_user}" + password: "${DB_PASSWORD:}" + sql: + init: + mode: always + encoding: UTF-8 + servlet: + multipart: + max-file-size: 5MB + max-request-size: 5MB + +mybatis: + mapper-locations: classpath*:/mapper/**/*.xml + configuration: + map-underscore-to-camel-case: true hera: jwt-secret: "replace-this-development-secret-replace-this-development-secret" + upload-dir: "${HERA_UPLOAD_DIR:./uploads}" wx: app-id: "${WECHAT_APP_ID:wxa76a2ef9e8ba612e}" app-secret: "${WECHAT_APP_SECRET:}" diff --git a/src/main/resources/mapper/LoginSessionMapper.xml b/src/main/resources/mapper/LoginSessionMapper.xml new file mode 100644 index 0000000..08eb50f --- /dev/null +++ b/src/main/resources/mapper/LoginSessionMapper.xml @@ -0,0 +1,16 @@ + + + + + + INSERT INTO login_session (session_key, open_id, union_id, expires_at) + VALUES (#{sessionKey}, #{openId}, #{unionId}, #{expiresAt}) + + + + diff --git a/src/main/resources/mapper/NfcChipMapper.xml b/src/main/resources/mapper/NfcChipMapper.xml new file mode 100644 index 0000000..42fe8a5 --- /dev/null +++ b/src/main/resources/mapper/NfcChipMapper.xml @@ -0,0 +1,47 @@ + + + + + + id, uid, chip_type, manufacturer, capacity, ndef_content, holder_type, virtual_item_id, + activated_at, last_login_at, created_at, updated_at + + + + + + + + INSERT INTO nfc_chip ( + uid, chip_type, manufacturer, capacity, ndef_content, holder_type, virtual_item_id, + activated_at, last_login_at + ) VALUES ( + #{uid}, #{chipType}, #{manufacturer}, #{capacity}, #{ndefContent}, #{holderType}, + #{virtualItemId}, #{activatedAt}, #{lastLoginAt} + ) + + + + UPDATE nfc_chip + SET uid = #{uid}, + chip_type = #{chipType}, + manufacturer = #{manufacturer}, + capacity = #{capacity}, + ndef_content = #{ndefContent}, + holder_type = #{holderType}, + virtual_item_id = #{virtualItemId}, + activated_at = #{activatedAt}, + last_login_at = #{lastLoginAt} + WHERE id = #{id} + + + DELETE FROM nfc_chip WHERE id = #{id} + diff --git a/src/main/resources/mapper/SmsCodeMapper.xml b/src/main/resources/mapper/SmsCodeMapper.xml new file mode 100644 index 0000000..194317b --- /dev/null +++ b/src/main/resources/mapper/SmsCodeMapper.xml @@ -0,0 +1,26 @@ + + + + + + INSERT INTO sms_code (phone, code, expires_at, used) + VALUES (#{phone}, #{code}, #{expiresAt}, #{used}) + + + + + + UPDATE sms_code + SET used = TRUE + WHERE id = #{id} + + diff --git a/src/main/resources/mapper/UserAccountMapper.xml b/src/main/resources/mapper/UserAccountMapper.xml new file mode 100644 index 0000000..b393b54 --- /dev/null +++ b/src/main/resources/mapper/UserAccountMapper.xml @@ -0,0 +1,53 @@ + + + + + + id, username, nickname, password_hash, mobile, email, nfc_uid, wechat_openid, wechat_unionid, + status, register_country_code, register_region_code, register_city, register_timezone, + created_at, updated_at + + + + + + + + INSERT INTO user_account ( + username, nickname, password_hash, mobile, email, nfc_uid, wechat_openid, wechat_unionid, + status, register_country_code, register_region_code, register_city, register_timezone + ) VALUES ( + #{username}, #{nickname}, #{passwordHash}, #{mobile}, #{email}, #{nfcUid}, #{wechatOpenid}, + #{wechatUnionid}, COALESCE(#{status}, 1), #{registerCountryCode}, #{registerRegionCode}, + #{registerCity}, #{registerTimezone} + ) + + + + UPDATE user_account + SET username = #{username}, + nickname = #{nickname}, + password_hash = COALESCE(#{passwordHash}, password_hash), + mobile = #{mobile}, + email = #{email}, + nfc_uid = #{nfcUid}, + wechat_openid = #{wechatOpenid}, + wechat_unionid = #{wechatUnionid}, + status = COALESCE(#{status}, status), + register_country_code = #{registerCountryCode}, + register_region_code = #{registerRegionCode}, + register_city = #{registerCity}, + register_timezone = #{registerTimezone} + WHERE id = #{id} + + + DELETE FROM user_account WHERE id = #{id} + diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..fca47eb --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,52 @@ + + + + + + id, open_id, union_id, phone, avatar_url, nickname, gender, birthday, birth_hour, + province, city, district, points, pinned_daily_card_id, profile_edit_count, created_at, updated_at + + + + + + + + INSERT INTO hera_user ( + open_id, union_id, phone, avatar_url, nickname, gender, birthday, birth_hour, + province, city, district, points, pinned_daily_card_id, profile_edit_count, created_at, updated_at + ) VALUES ( + #{openId}, #{unionId}, #{phone}, #{avatarUrl}, #{nickname}, #{gender}, #{birthday}, #{birthHour}, + #{province}, #{city}, #{district}, #{points}, #{pinnedDailyCardId}, #{profileEditCount}, + #{createdAt}, #{updatedAt} + ) + + + + UPDATE hera_user + SET union_id = #{unionId}, + phone = #{phone}, + avatar_url = #{avatarUrl}, + nickname = #{nickname}, + gender = #{gender}, + birthday = #{birthday}, + birth_hour = #{birthHour}, + province = #{province}, + city = #{city}, + district = #{district}, + points = #{points}, + pinned_daily_card_id = #{pinnedDailyCardId}, + profile_edit_count = #{profileEditCount}, + updated_at = #{updatedAt} + WHERE id = #{id} + + diff --git a/src/main/resources/mapper/UserProfileMapper.xml b/src/main/resources/mapper/UserProfileMapper.xml new file mode 100644 index 0000000..fcc58dd --- /dev/null +++ b/src/main/resources/mapper/UserProfileMapper.xml @@ -0,0 +1,63 @@ + + + + + + id, user_id, real_name_encrypted, gender, birthday, birth_shichen, birth_country_code, + birth_region_code, birth_city_code, birth_district_code, id_card_no_encrypted, id_card_hash, + id_verified, id_verified_at, avatar_url, residence_province, residence_city, residence_district, + residence_address, signature, created_at, updated_at + + + + + + + + INSERT INTO user_profile ( + user_id, real_name_encrypted, gender, birthday, birth_shichen, birth_country_code, + birth_region_code, birth_city_code, birth_district_code, id_card_no_encrypted, id_card_hash, + id_verified, id_verified_at, avatar_url, residence_province, residence_city, residence_district, + residence_address, signature + ) VALUES ( + #{userId}, #{realNameEncrypted}, COALESCE(#{gender}, 0), #{birthday}, #{birthShichen}, + #{birthCountryCode}, #{birthRegionCode}, #{birthCityCode}, #{birthDistrictCode}, + #{idCardNoEncrypted}, #{idCardHash}, COALESCE(#{idVerified}, 0), #{idVerifiedAt}, #{avatarUrl}, + #{residenceProvince}, #{residenceCity}, #{residenceDistrict}, #{residenceAddress}, #{signature} + ) + + + + UPDATE user_profile + SET user_id = #{userId}, + real_name_encrypted = #{realNameEncrypted}, + gender = COALESCE(#{gender}, gender), + birthday = #{birthday}, + birth_shichen = #{birthShichen}, + birth_country_code = #{birthCountryCode}, + birth_region_code = #{birthRegionCode}, + birth_city_code = #{birthCityCode}, + birth_district_code = #{birthDistrictCode}, + id_card_no_encrypted = #{idCardNoEncrypted}, + id_card_hash = #{idCardHash}, + id_verified = COALESCE(#{idVerified}, id_verified), + id_verified_at = #{idVerifiedAt}, + avatar_url = #{avatarUrl}, + residence_province = #{residenceProvince}, + residence_city = #{residenceCity}, + residence_district = #{residenceDistrict}, + residence_address = #{residenceAddress}, + signature = #{signature} + WHERE id = #{id} + + + DELETE FROM user_profile WHERE id = #{id} + diff --git a/src/main/resources/mapper/UserVirtualItemMapper.xml b/src/main/resources/mapper/UserVirtualItemMapper.xml new file mode 100644 index 0000000..e211869 --- /dev/null +++ b/src/main/resources/mapper/UserVirtualItemMapper.xml @@ -0,0 +1,41 @@ + + + + + + id, user_id, virtual_item_id, nfc_uid, sort_order, status, obtained_at, created_at, updated_at + + + + + + + + INSERT INTO user_virtual_item (user_id, virtual_item_id, nfc_uid, sort_order, status, obtained_at) + VALUES ( + #{userId}, #{virtualItemId}, #{nfcUid}, COALESCE(#{sortOrder}, 0), COALESCE(#{status}, 1), + COALESCE(#{obtainedAt}, CURRENT_TIMESTAMP) + ) + + + + UPDATE user_virtual_item + SET user_id = #{userId}, + virtual_item_id = #{virtualItemId}, + nfc_uid = #{nfcUid}, + sort_order = COALESCE(#{sortOrder}, sort_order), + status = COALESCE(#{status}, status), + obtained_at = COALESCE(#{obtainedAt}, obtained_at) + WHERE id = #{id} + + + DELETE FROM user_virtual_item WHERE id = #{id} + diff --git a/src/main/resources/mapper/UserVirtualItemOfferingMapper.xml b/src/main/resources/mapper/UserVirtualItemOfferingMapper.xml new file mode 100644 index 0000000..cf1dc90 --- /dev/null +++ b/src/main/resources/mapper/UserVirtualItemOfferingMapper.xml @@ -0,0 +1,46 @@ + + + + + + id, user_virtual_item_id, user_id, virtual_item_id, offering_type, content, merit_value, + status, offered_at, created_at, updated_at + + + + + + + + INSERT INTO user_virtual_item_offering ( + user_virtual_item_id, user_id, virtual_item_id, offering_type, content, merit_value, + status, offered_at + ) VALUES ( + #{userVirtualItemId}, #{userId}, #{virtualItemId}, #{offeringType}, #{content}, #{meritValue}, + COALESCE(#{status}, 1), COALESCE(#{offeredAt}, CURRENT_TIMESTAMP) + ) + + + + UPDATE user_virtual_item_offering + SET user_virtual_item_id = #{userVirtualItemId}, + user_id = #{userId}, + virtual_item_id = #{virtualItemId}, + offering_type = #{offeringType}, + content = #{content}, + merit_value = #{meritValue}, + status = COALESCE(#{status}, status), + offered_at = COALESCE(#{offeredAt}, offered_at) + WHERE id = #{id} + + + DELETE FROM user_virtual_item_offering WHERE id = #{id} + diff --git a/src/main/resources/mapper/UserVirtualItemTransferMapper.xml b/src/main/resources/mapper/UserVirtualItemTransferMapper.xml new file mode 100644 index 0000000..e831379 --- /dev/null +++ b/src/main/resources/mapper/UserVirtualItemTransferMapper.xml @@ -0,0 +1,49 @@ + + + + + + id, from_user_id, from_user_name, to_user_id, to_user_name, virtual_item_id, + from_user_item_id, to_user_item_id, content, status, transferred_at, created_at, updated_at + + + + + + + + INSERT INTO user_virtual_item_transfer ( + from_user_id, from_user_name, to_user_id, to_user_name, virtual_item_id, + from_user_item_id, to_user_item_id, content, status, transferred_at + ) VALUES ( + #{fromUserId}, #{fromUserName}, #{toUserId}, #{toUserName}, #{virtualItemId}, + #{fromUserItemId}, #{toUserItemId}, #{content}, COALESCE(#{status}, 1), + COALESCE(#{transferredAt}, CURRENT_TIMESTAMP) + ) + + + + UPDATE user_virtual_item_transfer + SET from_user_id = #{fromUserId}, + from_user_name = #{fromUserName}, + to_user_id = #{toUserId}, + to_user_name = #{toUserName}, + virtual_item_id = #{virtualItemId}, + from_user_item_id = #{fromUserItemId}, + to_user_item_id = #{toUserItemId}, + content = #{content}, + status = COALESCE(#{status}, status), + transferred_at = COALESCE(#{transferredAt}, transferred_at) + WHERE id = #{id} + + + DELETE FROM user_virtual_item_transfer WHERE id = #{id} + diff --git a/src/main/resources/mapper/VirtualItemMapper.xml b/src/main/resources/mapper/VirtualItemMapper.xml new file mode 100644 index 0000000..28e662f --- /dev/null +++ b/src/main/resources/mapper/VirtualItemMapper.xml @@ -0,0 +1,39 @@ + + + + + + id, item_name, icon_url, description, sort_order, enabled, created_at, updated_at + + + + + + + + INSERT INTO virtual_item (id, item_name, icon_url, description, sort_order, enabled) + VALUES ( + #{id}, #{itemName}, #{iconUrl}, #{description}, COALESCE(#{sortOrder}, 0), COALESCE(#{enabled}, 1) + ) + + + + UPDATE virtual_item + SET item_name = #{itemName}, + icon_url = #{iconUrl}, + description = #{description}, + sort_order = COALESCE(#{sortOrder}, sort_order), + enabled = COALESCE(#{enabled}, enabled) + WHERE id = #{id} + + + DELETE FROM virtual_item WHERE id = #{id} + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..268755b --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,190 @@ +CREATE TABLE IF NOT EXISTS hera_user ( + id BIGINT NOT NULL AUTO_INCREMENT, + open_id VARCHAR(128) NOT NULL, + union_id VARCHAR(128) NULL, + phone VARCHAR(32) NULL, + avatar_url VARCHAR(512) NULL, + nickname VARCHAR(64) NOT NULL, + gender VARCHAR(16) NULL, + birthday DATE NULL, + birth_hour VARCHAR(32) NULL, + province VARCHAR(64) NULL, + city VARCHAR(64) NULL, + district VARCHAR(64) NULL, + points INT NOT NULL DEFAULT 0, + pinned_daily_card_id VARCHAR(64) NULL, + profile_edit_count INT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_hera_user_open_id (open_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS login_session ( + id BIGINT NOT NULL AUTO_INCREMENT, + session_key VARCHAR(64) NOT NULL, + open_id VARCHAR(128) NOT NULL, + union_id VARCHAR(128) NULL, + expires_at DATETIME NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_login_session_key (session_key), + KEY idx_login_session_open_id (open_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS sms_code ( + id BIGINT NOT NULL AUTO_INCREMENT, + phone VARCHAR(32) NOT NULL, + code VARCHAR(16) NOT NULL, + expires_at DATETIME NOT NULL, + used TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (id), + KEY idx_sms_code_lookup (phone, code, used, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS nfc_chip ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + uid VARCHAR(64) NOT NULL, + chip_type VARCHAR(50) NULL, + manufacturer VARCHAR(50) NULL, + capacity INT UNSIGNED NULL, + ndef_content TEXT NULL, + holder_type TINYINT UNSIGNED NULL, + virtual_item_id TINYINT UNSIGNED NULL, + activated_at DATETIME NULL, + last_login_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_uid (uid), + KEY idx_holder_type (holder_type), + KEY idx_virtual_item_id (virtual_item_id), + KEY idx_activated_at (activated_at), + KEY idx_last_login_at (last_login_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS user_account ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + username VARCHAR(50) NOT NULL, + nickname VARCHAR(50) NULL, + password_hash VARCHAR(100) NOT NULL, + mobile VARCHAR(20) NULL, + email VARCHAR(100) NULL, + nfc_uid VARCHAR(64) NULL, + wechat_openid VARCHAR(64) NULL, + wechat_unionid VARCHAR(64) NULL, + status TINYINT NOT NULL DEFAULT 1, + register_country_code CHAR(2) NULL, + register_region_code VARCHAR(20) NULL, + register_city VARCHAR(100) NULL, + register_timezone VARCHAR(50) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_username (username), + UNIQUE KEY uk_mobile (mobile), + UNIQUE KEY uk_email (email), + UNIQUE KEY uk_nfc_uid (nfc_uid), + UNIQUE KEY uk_wechat_openid (wechat_openid), + KEY idx_wechat_unionid (wechat_unionid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS user_profile ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + real_name_encrypted VARCHAR(128) NULL, + gender TINYINT NOT NULL DEFAULT 0, + birthday DATE NULL, + birth_shichen TINYINT UNSIGNED NULL, + birth_country_code CHAR(2) NULL, + birth_region_code VARCHAR(20) NULL, + birth_city_code VARCHAR(20) NULL, + birth_district_code VARCHAR(20) NULL, + id_card_no_encrypted VARCHAR(128) NULL, + id_card_hash CHAR(64) NULL, + id_verified TINYINT NOT NULL DEFAULT 0, + id_verified_at DATETIME NULL, + avatar_url VARCHAR(255) NULL, + residence_province VARCHAR(50) NULL, + residence_city VARCHAR(50) NULL, + residence_district VARCHAR(50) NULL, + residence_address VARCHAR(255) NULL, + signature VARCHAR(500) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_user_id (user_id), + UNIQUE KEY uk_id_card_hash (id_card_hash), + KEY idx_id_verified (id_verified), + CONSTRAINT fk_profile_user FOREIGN KEY (user_id) REFERENCES user_account (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS virtual_item ( + id INT UNSIGNED NOT NULL, + item_name VARCHAR(50) NOT NULL, + icon_url VARCHAR(255) NULL, + description VARCHAR(255) NULL, + sort_order SMALLINT NOT NULL DEFAULT 0, + enabled TINYINT NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_enabled_sort (enabled, sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS user_virtual_item ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + virtual_item_id TINYINT UNSIGNED NOT NULL, + nfc_uid VARCHAR(64) NULL, + sort_order INT NOT NULL DEFAULT 0, + status TINYINT NOT NULL DEFAULT 1, + obtained_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_user_item (user_id, virtual_item_id), + KEY idx_user_sort (user_id, sort_order), + KEY idx_virtual_item_id (virtual_item_id), + KEY idx_nfc_uid (nfc_uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS user_virtual_item_offering ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_virtual_item_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + virtual_item_id TINYINT UNSIGNED NOT NULL, + offering_type TINYINT NOT NULL, + content VARCHAR(500) NULL, + merit_value INT NULL, + status TINYINT NOT NULL DEFAULT 1, + offered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_user_id (user_id), + KEY idx_user_virtual_item_id (user_virtual_item_id), + KEY idx_virtual_item_id (virtual_item_id), + KEY idx_offering_type (offering_type), + KEY idx_offered_at (offered_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS user_virtual_item_transfer ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + from_user_id BIGINT UNSIGNED NOT NULL, + from_user_name VARCHAR(50) NULL, + to_user_id BIGINT UNSIGNED NOT NULL, + to_user_name VARCHAR(50) NULL, + virtual_item_id TINYINT UNSIGNED NOT NULL, + from_user_item_id BIGINT UNSIGNED NULL, + to_user_item_id BIGINT UNSIGNED NULL, + content VARCHAR(500) NULL, + status TINYINT NOT NULL DEFAULT 1, + transferred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_from_user_id (from_user_id), + KEY idx_to_user_id (to_user_id), + KEY idx_virtual_item_id (virtual_item_id), + KEY idx_transferred_at (transferred_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;