feat: add MyBatis CRUD and database integration

This commit is contained in:
zhouzhibo committed 2026-10-14 22:13:00 +08:00
1 parent 4222b7124c
commit 8a97a0b054
67 files changed
+2597 -102

No files matched your search

+72
View File
@@ -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<T>`,业务错误统一抛出 `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 时,还应在可用的测试数据库上至少验证对应的查询和写入链路;写入的临时数据必须在验证结束后清理。
+18
View File
@@ -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 <token>`,并支持统一操作:
- `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`。
+5 -4
View File
@@ -27,12 +27,13 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
@@ -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));
}
}
@@ -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;
}
@@ -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<LoginSession> findBySessionKey(String sessionKey);
}
@@ -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<LoginSession, Long> {
Optional<LoginSession> findBySessionKey(String sessionKey);
}
@@ -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;
}
@@ -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<SmsCode> findLatestUnused(
@Param("phone") String phone,
@Param("code") String code
);
int markUsed(Long id);
}
@@ -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<SmsCode, Long> {
Optional<SmsCode> findTopByPhoneAndCodeAndUsedFalseOrderByExpiresAtDesc(String phone, String code);
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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<T extends BaseEntity> {
private final CrudService<T> service;
protected CrudController(CrudService<T> service) {
this.service = service;
}
@GetMapping("/{id}")
public ApiResponse<T> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@GetMapping
public ApiResponse<List<T>> list(
@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "20") int limit
) {
return ApiResponse.ok(service.list(offset, limit));
}
@PostMapping
public ApiResponse<T> create(@RequestBody T entity) {
return ApiResponse.ok(service.create(entity));
}
@PutMapping("/{id}")
public ApiResponse<T> update(@PathVariable Long id, @RequestBody T entity) {
return ApiResponse.ok(service.update(id, entity));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok(null);
}
}
@@ -0,0 +1,17 @@
package com.hongzhen.hera.common;
import java.util.List;
import java.util.Optional;
public interface CrudMapper<T extends BaseEntity> {
Optional<T> findById(Long id);
List<T> findAll(int offset, int limit);
int insert(T entity);
int update(T entity);
int deleteById(Long id);
}
@@ -0,0 +1,50 @@
package com.hongzhen.hera.common;
import java.util.List;
import org.springframework.transaction.annotation.Transactional;
public abstract class CrudService<T extends BaseEntity> {
private static final int MAX_PAGE_SIZE = 100;
private final CrudMapper<T> mapper;
private final String entityName;
protected CrudService(CrudMapper<T> mapper, String entityName) {
this.mapper = mapper;
this.entityName = entityName;
}
public T get(Long id) {
return mapper.findById(id).orElseThrow(() -> new BusinessException(entityName + "不存在"));
}
public List<T> 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 + "不存在");
}
}
}
@@ -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<Void> handleValidation(Exception exception) {
return ApiResponse.fail("参数不正确");
}
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ApiResponse<Void> handleDataIntegrity(DataIntegrityViolationException exception) {
return ApiResponse.fail("数据重复或关联关系不正确");
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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) {
@@ -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;
}
}
@@ -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<NfcChip> {
public NfcChipController(NfcChipService service) {
super(service);
}
}
@@ -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<NfcChip> {
@Override
Optional<NfcChip> findById(Long id);
@Override
List<NfcChip> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(NfcChip entity);
@Override
int update(NfcChip entity);
@Override
int deleteById(Long id);
}
@@ -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<NfcChip> {
public NfcChipService(NfcChipMapper mapper) {
super(mapper, "NFC芯片");
}
}
@@ -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;
}
}
@@ -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<UserAccount> {
public UserAccountController(UserAccountService service) {
super(service);
}
}
@@ -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<UserAccount> {
@Override
Optional<UserAccount> findById(Long id);
@Override
List<UserAccount> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(UserAccount entity);
@Override
int update(UserAccount entity);
@Override
int deleteById(Long id);
}
@@ -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<UserAccount> {
public UserAccountService(UserAccountMapper mapper) {
super(mapper, "用户账号");
}
}
@@ -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;
}
}
@@ -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<UserProfile> {
public UserProfileController(UserProfileService service) {
super(service);
}
}
@@ -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<UserProfile> {
@Override
Optional<UserProfile> findById(Long id);
@Override
List<UserProfile> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(UserProfile entity);
@Override
int update(UserProfile entity);
@Override
int deleteById(Long id);
}
@@ -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<UserProfile> {
public UserProfileService(UserProfileMapper mapper) {
super(mapper, "用户资料");
}
}
@@ -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;
}
}
@@ -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<UserVirtualItem> {
public UserVirtualItemController(UserVirtualItemService service) {
super(service);
}
}
@@ -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<UserVirtualItem> {
@Override
Optional<UserVirtualItem> findById(Long id);
@Override
List<UserVirtualItem> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(UserVirtualItem entity);
@Override
int update(UserVirtualItem entity);
@Override
int deleteById(Long id);
}
@@ -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;
}
}
@@ -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<UserVirtualItemOffering> {
public UserVirtualItemOfferingController(UserVirtualItemOfferingService service) {
super(service);
}
}
@@ -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<UserVirtualItemOffering> {
@Override
Optional<UserVirtualItemOffering> findById(Long id);
@Override
List<UserVirtualItemOffering> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(UserVirtualItemOffering entity);
@Override
int update(UserVirtualItemOffering entity);
@Override
int deleteById(Long id);
}
@@ -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<UserVirtualItemOffering> {
public UserVirtualItemOfferingService(UserVirtualItemOfferingMapper mapper) {
super(mapper, "供奉记录");
}
}
@@ -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<UserVirtualItem> {
public UserVirtualItemService(UserVirtualItemMapper mapper) {
super(mapper, "用户虚拟物");
}
}
@@ -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;
}
}
@@ -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<UserVirtualItemTransfer> {
public UserVirtualItemTransferController(UserVirtualItemTransferService service) {
super(service);
}
}
@@ -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<UserVirtualItemTransfer> {
@Override
Optional<UserVirtualItemTransfer> findById(Long id);
@Override
List<UserVirtualItemTransfer> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(UserVirtualItemTransfer entity);
@Override
int update(UserVirtualItemTransfer entity);
@Override
int deleteById(Long id);
}
@@ -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<UserVirtualItemTransfer> {
public UserVirtualItemTransferService(UserVirtualItemTransferMapper mapper) {
super(mapper, "回向记录");
}
}
@@ -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;
}
}
@@ -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<VirtualItem> {
public VirtualItemController(VirtualItemService service) {
super(service);
}
}
@@ -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<VirtualItem> {
@Override
Optional<VirtualItem> findById(Long id);
@Override
List<VirtualItem> findAll(@Param("offset") int offset, @Param("limit") int limit);
@Override
int insert(VirtualItem entity);
@Override
int update(VirtualItem entity);
@Override
int deleteById(Long id);
}
@@ -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<VirtualItem> {
public VirtualItemService(VirtualItemMapper mapper) {
super(mapper, "虚拟物");
}
}
@@ -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("请先登录");
@@ -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;
}
}
@@ -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;
}
+89 -11
View File
@@ -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();
}
@@ -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<ProfileResponse> update(@Valid @RequestBody UpdateProfileRequest request) {
return ApiResponse.ok(userService.updateProfile(CurrentUser.id(), request));
}
@PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<ProfileResponse> uploadAvatar(@RequestParam("file") MultipartFile file) {
String avatarUrl = avatarStorageService.store(CurrentUser.id(), file);
return ApiResponse.ok(userService.updateAvatar(CurrentUser.id(), avatarUrl));
}
}
@@ -0,0 +1,16 @@
package com.hongzhen.hera.user;
import java.util.Optional;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
Optional<User> findById(Long id);
Optional<User> findByOpenId(String openId);
int insert(User user);
int update(User user);
}
@@ -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<User, Long> {
Optional<User> findByOpenId(String openId);
}
@@ -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(),
+18 -13
View File
@@ -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:}"
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.auth.LoginSessionMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO login_session (session_key, open_id, union_id, expires_at)
VALUES (#{sessionKey}, #{openId}, #{unionId}, #{expiresAt})
</insert>
<select id="findBySessionKey" resultType="com.hongzhen.hera.auth.LoginSession">
SELECT id, session_key, open_id, union_id, expires_at
FROM login_session
WHERE session_key = #{sessionKey}
</select>
</mapper>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.NfcChipMapper">
<sql id="BaseColumnList">
id, uid, chip_type, manufacturer, capacity, ndef_content, holder_type, virtual_item_id,
activated_at, last_login_at, created_at, updated_at
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.NfcChip">
SELECT <include refid="BaseColumnList"/> FROM nfc_chip WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.NfcChip">
SELECT <include refid="BaseColumnList"/>
FROM nfc_chip
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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}
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM nfc_chip WHERE id = #{id}</delete>
</mapper>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.auth.SmsCodeMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO sms_code (phone, code, expires_at, used)
VALUES (#{phone}, #{code}, #{expiresAt}, #{used})
</insert>
<select id="findLatestUnused" resultType="com.hongzhen.hera.auth.SmsCode">
SELECT id, phone, code, expires_at, used
FROM sms_code
WHERE phone = #{phone}
AND code = #{code}
AND used = FALSE
ORDER BY expires_at DESC
LIMIT 1
</select>
<update id="markUsed">
UPDATE sms_code
SET used = TRUE
WHERE id = #{id}
</update>
</mapper>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.UserAccountMapper">
<sql id="BaseColumnList">
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
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.UserAccount">
SELECT <include refid="BaseColumnList"/> FROM user_account WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.UserAccount">
SELECT <include refid="BaseColumnList"/>
FROM user_account
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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}
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM user_account WHERE id = #{id}</delete>
</mapper>
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.user.UserMapper">
<sql id="BaseColumnList">
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
</sql>
<select id="findById" resultType="com.hongzhen.hera.user.User">
SELECT <include refid="BaseColumnList"/>
FROM hera_user
WHERE id = #{id}
</select>
<select id="findByOpenId" resultType="com.hongzhen.hera.user.User">
SELECT <include refid="BaseColumnList"/>
FROM hera_user
WHERE open_id = #{openId}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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}
)
</insert>
<update id="update">
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}
</update>
</mapper>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.UserProfileMapper">
<sql id="BaseColumnList">
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
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.UserProfile">
SELECT <include refid="BaseColumnList"/> FROM user_profile WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.UserProfile">
SELECT <include refid="BaseColumnList"/>
FROM user_profile
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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}
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM user_profile WHERE id = #{id}</delete>
</mapper>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.UserVirtualItemMapper">
<sql id="BaseColumnList">
id, user_id, virtual_item_id, nfc_uid, sort_order, status, obtained_at, created_at, updated_at
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.UserVirtualItem">
SELECT <include refid="BaseColumnList"/> FROM user_virtual_item WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.UserVirtualItem">
SELECT <include refid="BaseColumnList"/>
FROM user_virtual_item
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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)
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM user_virtual_item WHERE id = #{id}</delete>
</mapper>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.UserVirtualItemOfferingMapper">
<sql id="BaseColumnList">
id, user_virtual_item_id, user_id, virtual_item_id, offering_type, content, merit_value,
status, offered_at, created_at, updated_at
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.UserVirtualItemOffering">
SELECT <include refid="BaseColumnList"/> FROM user_virtual_item_offering WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.UserVirtualItemOffering">
SELECT <include refid="BaseColumnList"/>
FROM user_virtual_item_offering
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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)
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM user_virtual_item_offering WHERE id = #{id}</delete>
</mapper>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.UserVirtualItemTransferMapper">
<sql id="BaseColumnList">
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
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.UserVirtualItemTransfer">
SELECT <include refid="BaseColumnList"/> FROM user_virtual_item_transfer WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.UserVirtualItemTransfer">
SELECT <include refid="BaseColumnList"/>
FROM user_virtual_item_transfer
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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)
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM user_virtual_item_transfer WHERE id = #{id}</delete>
</mapper>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hongzhen.hera.data.VirtualItemMapper">
<sql id="BaseColumnList">
id, item_name, icon_url, description, sort_order, enabled, created_at, updated_at
</sql>
<select id="findById" resultType="com.hongzhen.hera.data.VirtualItem">
SELECT <include refid="BaseColumnList"/> FROM virtual_item WHERE id = #{id}
</select>
<select id="findAll" resultType="com.hongzhen.hera.data.VirtualItem">
SELECT <include refid="BaseColumnList"/>
FROM virtual_item
ORDER BY sort_order, id
LIMIT #{offset}, #{limit}
</select>
<insert id="insert">
INSERT INTO virtual_item (id, item_name, icon_url, description, sort_order, enabled)
VALUES (
#{id}, #{itemName}, #{iconUrl}, #{description}, COALESCE(#{sortOrder}, 0), COALESCE(#{enabled}, 1)
)
</insert>
<update id="update">
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}
</update>
<delete id="deleteById">DELETE FROM virtual_item WHERE id = #{id}</delete>
</mapper>
+190
View File
@@ -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;