feat: support birthday calendar and named worship dedications
This commit is contained in:
1 parent
30c5f9f114
commit
0f927c1884
21 files changed
+329
-31
No files matched your search
@@ -30,6 +30,9 @@ public class UserProfile extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private LocalDate birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
|
/** 生日显示历法:solar 阳历,lunar 农历,birthday 始终存阳历日期。 */
|
||||||
|
private String birthdayCalendar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 出生时辰,取值范围为 0 至 11。
|
* 出生时辰,取值范围为 0 至 11。
|
||||||
*/
|
*/
|
||||||
@@ -164,6 +167,14 @@ public class UserProfile extends BaseEntity {
|
|||||||
this.birthday = birthday;
|
this.birthday = birthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBirthdayCalendar() {
|
||||||
|
return birthdayCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBirthdayCalendar(String birthdayCalendar) {
|
||||||
|
this.birthdayCalendar = birthdayCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getBirthShichen() {
|
public Integer getBirthShichen() {
|
||||||
return birthShichen;
|
return birthShichen;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class UserVirtualItemTransfer extends BaseEntity {
|
|||||||
private String fromUserName;
|
private String fromUserName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收用户 ID。
|
* 接收用户 ID,按任意姓名回向时为空,不伪造接收账号。
|
||||||
*/
|
*/
|
||||||
private Long toUserId;
|
private Long toUserId;
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public final class NfcDtos {
|
|||||||
@NotNull @Positive
|
@NotNull @Positive
|
||||||
private Long userVirtualItemId;
|
private Long userVirtualItemId;
|
||||||
/**
|
/**
|
||||||
* 1 请神,2 上香,3 祈愿,4 功德,5 送神。
|
* 1 请神,2 上香,3 祈愿,5 送神,4 本期不开放。
|
||||||
*/
|
*/
|
||||||
@NotNull @Min(1) @Max(5)
|
@NotNull @Min(1) @Max(5)
|
||||||
private Integer offeringType;
|
private Integer offeringType;
|
||||||
@@ -94,9 +94,9 @@ public final class NfcDtos {
|
|||||||
@Size(max = 500)
|
@Size(max = 500)
|
||||||
private String content;
|
private String content;
|
||||||
/**
|
/**
|
||||||
* 仅功德行为填写,范围为 1 至 1000000。
|
* 保留字段,本期不支持填写。
|
||||||
*/
|
*/
|
||||||
@Min(1) @Max(1000000)
|
@javax.validation.constraints.Null(message = "本期不支持功德功能")
|
||||||
private Integer meritValue;
|
private Integer meritValue;
|
||||||
public Long getUserVirtualItemId() { return userVirtualItemId; }
|
public Long getUserVirtualItemId() { return userVirtualItemId; }
|
||||||
public void setUserVirtualItemId(Long id) { this.userVirtualItemId = id; }
|
public void setUserVirtualItemId(Long id) { this.userVirtualItemId = id; }
|
||||||
@@ -135,6 +135,25 @@ public final class NfcDtos {
|
|||||||
public void setContent(String content) { this.content = content; }
|
public void setContent(String content) { this.content = content; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 面向任意姓名的回向,不要求接收人是系统账号。 */
|
||||||
|
public static class NamedDedicationRequest {
|
||||||
|
/** 当前用户持有关系 ID。 */
|
||||||
|
@NotNull @Positive
|
||||||
|
private Long userVirtualItemId;
|
||||||
|
/** 回向对象姓名,只作记录,不据此关联账号。 */
|
||||||
|
@NotBlank @Size(max = 50)
|
||||||
|
private String name;
|
||||||
|
/** 回向祝愿。 */
|
||||||
|
@NotBlank @Size(max = 500)
|
||||||
|
private String content;
|
||||||
|
public Long getUserVirtualItemId() { return userVirtualItemId; }
|
||||||
|
public void setUserVirtualItemId(Long id) { this.userVirtualItemId = id; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getContent() { return content; }
|
||||||
|
public void setContent(String content) { this.content = content; }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量调整本人资产顺序,提交顺序对应从零开始的排序值。
|
* 批量调整本人资产顺序,提交顺序对应从零开始的排序值。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package com.hongzhen.hera.nfc;
|
|||||||
import com.hongzhen.hera.common.BusinessException;
|
import com.hongzhen.hera.common.BusinessException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 供奉行为类型,功德仅记录数值,不表示支付或可消费余额。
|
* 本期启用的供奉行为类型,编码 4 保留但不开放。
|
||||||
*/
|
*/
|
||||||
public enum OfferingType {
|
public enum OfferingType {
|
||||||
INVITE(1), INCENSE(2), WISH(3), MERIT(4), SEND_OFF(5);
|
INVITE(1), INCENSE(2), WISH(3), SEND_OFF(5);
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hongzhen.hera.nfc;
|
||||||
|
|
||||||
|
import com.hongzhen.hera.common.ApiResponse;
|
||||||
|
import com.hongzhen.hera.security.CurrentUser;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/** 天宫本人持有状态和计数查询。 */
|
||||||
|
@RestController
|
||||||
|
public class TiangongController {
|
||||||
|
private final VirtualAssetService service;
|
||||||
|
public TiangongController(VirtualAssetService service) { this.service = service; }
|
||||||
|
/** 只返回当前 JWT 账号的持有关系及统计,不接收客户端用户 ID。 */
|
||||||
|
@GetMapping("/api/tiangong/items")
|
||||||
|
public ApiResponse<List<TiangongItemView>> list() { return ApiResponse.ok(service.tiangong(CurrentUser.id())); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.hongzhen.hera.nfc;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
/** 天宫列表与详情共用的本人供奉视图,不包含功德或钱包信息。 */
|
||||||
|
public class TiangongItemView {
|
||||||
|
/** 神仙字典 ID,不是本人持有关系 ID。 */
|
||||||
|
private Integer id;
|
||||||
|
/** 后端配置的神仙名称。 */
|
||||||
|
private String name;
|
||||||
|
/** 后端配置的图标。 */
|
||||||
|
private String iconUrl;
|
||||||
|
/** 后端配置的介绍。 */
|
||||||
|
private String description;
|
||||||
|
/** 本人持有关系 ID,未持有时为空。 */
|
||||||
|
private Long holdingId;
|
||||||
|
/** 是否供奉中且仍有对应绑定设备。 */
|
||||||
|
private boolean active;
|
||||||
|
/** 本人最初取得该持有关系的时间。 */
|
||||||
|
private LocalDateTime obtainedAt;
|
||||||
|
/** 本人该持有关系的有效上香流水总数。 */
|
||||||
|
private long incenseCount;
|
||||||
|
public Integer getId() { return id; }
|
||||||
|
public void setId(Integer id) { this.id = id; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
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 Long getHoldingId() { return holdingId; }
|
||||||
|
public void setHoldingId(Long holdingId) { this.holdingId = holdingId; }
|
||||||
|
public boolean isActive() { return active; }
|
||||||
|
public void setActive(boolean active) { this.active = active; }
|
||||||
|
public LocalDateTime getObtainedAt() { return obtainedAt; }
|
||||||
|
public void setObtainedAt(LocalDateTime obtainedAt) { this.obtainedAt = obtainedAt; }
|
||||||
|
public long getIncenseCount() { return incenseCount; }
|
||||||
|
public void setIncenseCount(long incenseCount) { this.incenseCount = incenseCount; }
|
||||||
|
/** 按既有取得时间计算已满的供奉天数,不添加虚构的天数。 */
|
||||||
|
public long getDays() { return obtainedAt == null ? 0 : Math.max(0, ChronoUnit.DAYS.between(obtainedAt, VirtualAssetService.now())); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.hongzhen.hera.nfc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
/** 查询当前账号的神仙持有状态与上香统计。 */
|
||||||
|
@Mapper
|
||||||
|
public interface TiangongMapper {
|
||||||
|
/** 字典 ID 为 TINYINT,读取全部已启用神仙,避免分页漏掉持有关系。 */
|
||||||
|
List<TiangongItemView> findByUser(@Param("userId") Long userId);
|
||||||
|
}
|
||||||
@@ -85,6 +85,12 @@ public class VirtualAssetController {
|
|||||||
return ApiResponse.ok(service.dedicate(CurrentUser.id(), request));
|
return ApiResponse.ok(service.dedicate(CurrentUser.id(), request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 记录任意姓名的回向,不创建或转移接收方资产。 */
|
||||||
|
@PostMapping("/api/user/virtual-items/dedications")
|
||||||
|
public ApiResponse<UserVirtualItemTransfer> dedicateToName(@Valid @RequestBody NfcDtos.NamedDedicationRequest request) {
|
||||||
|
return ApiResponse.ok(service.dedicateToName(CurrentUser.id(), request));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取本人参与的回向历史。
|
* 获取本人参与的回向历史。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,16 +32,18 @@ public class VirtualAssetService {
|
|||||||
private final UserVirtualItemOfferingMapper offerings;
|
private final UserVirtualItemOfferingMapper offerings;
|
||||||
private final UserVirtualItemTransferMapper transfers;
|
private final UserVirtualItemTransferMapper transfers;
|
||||||
private final NfcChipMapper chips;
|
private final NfcChipMapper chips;
|
||||||
|
private final TiangongMapper tiangong;
|
||||||
|
|
||||||
public VirtualAssetService(UserAccountMapper accounts, VirtualItemMapper dictionary,
|
public VirtualAssetService(UserAccountMapper accounts, VirtualItemMapper dictionary,
|
||||||
UserVirtualItemMapper holdings, UserVirtualItemOfferingMapper offerings,
|
UserVirtualItemMapper holdings, UserVirtualItemOfferingMapper offerings,
|
||||||
UserVirtualItemTransferMapper transfers, NfcChipMapper chips) {
|
UserVirtualItemTransferMapper transfers, NfcChipMapper chips, TiangongMapper tiangong) {
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.dictionary = dictionary;
|
this.dictionary = dictionary;
|
||||||
this.holdings = holdings;
|
this.holdings = holdings;
|
||||||
this.offerings = offerings;
|
this.offerings = offerings;
|
||||||
this.transfers = transfers;
|
this.transfers = transfers;
|
||||||
this.chips = chips;
|
this.chips = chips;
|
||||||
|
this.tiangong = tiangong;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +66,12 @@ public class VirtualAssetService {
|
|||||||
return dictionary.findEnabled(Math.max(0, offset), NfcRules.limit(limit));
|
return dictionary.findEnabled(Math.max(0, offset), NfcRules.limit(limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取本人真实神仙状态及上香计数。 */
|
||||||
|
public List<TiangongItemView> tiangong(Long userId) {
|
||||||
|
account(userId, false);
|
||||||
|
return tiangong.findByUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询本人资产,包含送神或解绑失效记录,便于展示历史。
|
* 查询本人资产,包含送神或解绑失效记录,便于展示历史。
|
||||||
*/
|
*/
|
||||||
@@ -131,15 +139,18 @@ public class VirtualAssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录请神、上香、祈愿、功德或送神,状态变化与流水在同一事务提交。
|
* 记录请神、上香、祈愿或送神,状态变化与流水在同一事务提交。
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public UserVirtualItemOffering offer(Long userId, NfcDtos.OfferingRequest request) {
|
public UserVirtualItemOffering offer(Long userId, NfcDtos.OfferingRequest request) {
|
||||||
account(userId, true);
|
account(userId, true);
|
||||||
UserVirtualItem item = ownHolding(userId, request.getUserVirtualItemId());
|
UserVirtualItem item = ownHolding(userId, request.getUserVirtualItemId());
|
||||||
OfferingType type = OfferingType.from(request.getOfferingType());
|
OfferingType type = OfferingType.from(request.getOfferingType());
|
||||||
validateMerit(type, request.getMeritValue());
|
rejectMerit(request.getMeritValue());
|
||||||
validateContent(request.getContent());
|
validateContent(request.getContent());
|
||||||
|
if (type == OfferingType.WISH && !StringUtils.hasText(request.getContent())) {
|
||||||
|
throw new BusinessException("请先输入您的愿望");
|
||||||
|
}
|
||||||
enabled(item.getVirtualItemId());
|
enabled(item.getVirtualItemId());
|
||||||
requireBoundDevice(userId, item);
|
requireBoundDevice(userId, item);
|
||||||
if (type == OfferingType.INVITE) {
|
if (type == OfferingType.INVITE) {
|
||||||
@@ -168,14 +179,8 @@ public class VirtualAssetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateMerit(OfferingType type, Integer value) {
|
private void rejectMerit(Integer value) {
|
||||||
if (type == OfferingType.MERIT) {
|
if (value != null) { throw new BusinessException("本期不支持功德功能"); }
|
||||||
if (value == null || value < 1 || value > NfcRules.MAX_MERIT_VALUE) {
|
|
||||||
throw new BusinessException("功德值必须为 1 至 1000000");
|
|
||||||
}
|
|
||||||
} else if (value != null) {
|
|
||||||
throw new BusinessException("只有功德行为可以填写功德值");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateContent(String content) {
|
private void validateContent(String content) {
|
||||||
@@ -238,6 +243,32 @@ public class VirtualAssetService {
|
|||||||
return StringUtils.hasText(user.getNickname()) ? user.getNickname() : "用户" + user.getId();
|
return StringUtils.hasText(user.getNickname()) ? user.getNickname() : "用户" + user.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 记录任意姓名的回向祝愿,不查询接收账号、不转移资产。 */
|
||||||
|
@Transactional
|
||||||
|
public UserVirtualItemTransfer dedicateToName(Long userId, NfcDtos.NamedDedicationRequest request) {
|
||||||
|
UserAccount from = account(userId, true);
|
||||||
|
UserVirtualItem item = ownHolding(userId, request.getUserVirtualItemId());
|
||||||
|
requireActive(item);
|
||||||
|
enabled(item.getVirtualItemId());
|
||||||
|
requireBoundDevice(userId, item);
|
||||||
|
if (!StringUtils.hasText(request.getName()) || request.getName().trim().length() > 50
|
||||||
|
|| !StringUtils.hasText(request.getContent())) {
|
||||||
|
throw new BusinessException("请填写有效的回向对象和祝愿");
|
||||||
|
}
|
||||||
|
validateContent(request.getContent());
|
||||||
|
UserVirtualItemTransfer record = new UserVirtualItemTransfer();
|
||||||
|
record.setFromUserId(userId);
|
||||||
|
record.setFromUserName(displayName(from));
|
||||||
|
record.setToUserName(request.getName().trim());
|
||||||
|
record.setVirtualItemId(item.getVirtualItemId());
|
||||||
|
record.setFromUserItemId(item.getId());
|
||||||
|
record.setContent(request.getContent().trim());
|
||||||
|
record.setStatus(NfcRules.ACTIVE);
|
||||||
|
record.setTransferredAt(now());
|
||||||
|
transfers.insert(record);
|
||||||
|
return transfers.findById(record.getId()).orElseThrow(() -> new BusinessException("回向记录保存失败"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按客户端顺序设置本人资产的排序值,任一 ID 不合法则整批回滚。
|
* 按客户端顺序设置本人资产的排序值,任一 ID 不合法则整批回滚。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.List;
|
|||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import javax.validation.constraints.PastOrPresent;
|
import javax.validation.constraints.PastOrPresent;
|
||||||
|
import javax.validation.constraints.Pattern;
|
||||||
import javax.validation.constraints.Size;
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
public class ProfileDtos {
|
public class ProfileDtos {
|
||||||
@@ -16,6 +17,8 @@ public class ProfileDtos {
|
|||||||
private String avatarUrl;
|
private String avatarUrl;
|
||||||
private String gender;
|
private String gender;
|
||||||
private LocalDate birthday;
|
private LocalDate birthday;
|
||||||
|
/** 用户选择的生日显示历法,birthday 字段始终为阳历。 */
|
||||||
|
private String birthdayCalendar;
|
||||||
private String birthHour;
|
private String birthHour;
|
||||||
private String province;
|
private String province;
|
||||||
private String city;
|
private String city;
|
||||||
@@ -23,7 +26,7 @@ public class ProfileDtos {
|
|||||||
private int remainingEdits;
|
private int remainingEdits;
|
||||||
private ElementReport elementReport;
|
private ElementReport elementReport;
|
||||||
|
|
||||||
public ProfileResponse(Long id, String nickname, String phone, String avatarUrl, String gender, LocalDate birthday, String birthHour,
|
public ProfileResponse(Long id, String nickname, String phone, String avatarUrl, String gender, LocalDate birthday, String birthdayCalendar, String birthHour,
|
||||||
String province, String city, String district, int remainingEdits, ElementReport elementReport) {
|
String province, String city, String district, int remainingEdits, ElementReport elementReport) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
@@ -31,6 +34,7 @@ public class ProfileDtos {
|
|||||||
this.avatarUrl = avatarUrl;
|
this.avatarUrl = avatarUrl;
|
||||||
this.gender = gender;
|
this.gender = gender;
|
||||||
this.birthday = birthday;
|
this.birthday = birthday;
|
||||||
|
this.birthdayCalendar = birthdayCalendar;
|
||||||
this.birthHour = birthHour;
|
this.birthHour = birthHour;
|
||||||
this.province = province;
|
this.province = province;
|
||||||
this.city = city;
|
this.city = city;
|
||||||
@@ -63,6 +67,10 @@ public class ProfileDtos {
|
|||||||
return birthday;
|
return birthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBirthdayCalendar() {
|
||||||
|
return birthdayCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBirthHour() {
|
public String getBirthHour() {
|
||||||
return birthHour;
|
return birthHour;
|
||||||
}
|
}
|
||||||
@@ -182,6 +190,10 @@ public class ProfileDtos {
|
|||||||
@NotNull
|
@NotNull
|
||||||
@PastOrPresent
|
@PastOrPresent
|
||||||
private LocalDate birthday;
|
private LocalDate birthday;
|
||||||
|
/** 生日显示历法,可选 solar、lunar,未指定时使用阳历。 */
|
||||||
|
@NotBlank
|
||||||
|
@Pattern(regexp = "solar|lunar", message = "生日历法只能为阳历或农历")
|
||||||
|
private String birthdayCalendar = "solar";
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String birthHour;
|
private String birthHour;
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@@ -206,6 +218,10 @@ public class ProfileDtos {
|
|||||||
return birthday;
|
return birthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBirthdayCalendar() {
|
||||||
|
return birthdayCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBirthHour() {
|
public String getBirthHour() {
|
||||||
return birthHour;
|
return birthHour;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public class User {
|
|||||||
*/
|
*/
|
||||||
private LocalDate birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
|
/** 生日显示历法:solar 阳历,lunar 农历,生日日期本身始终为阳历。 */
|
||||||
|
private String birthdayCalendar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 出生时辰。
|
* 出生时辰。
|
||||||
*/
|
*/
|
||||||
@@ -173,6 +176,14 @@ public class User {
|
|||||||
this.birthday = birthday;
|
this.birthday = birthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBirthdayCalendar() {
|
||||||
|
return birthdayCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBirthdayCalendar(String birthdayCalendar) {
|
||||||
|
this.birthdayCalendar = birthdayCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBirthHour() {
|
public String getBirthHour() {
|
||||||
return birthHour;
|
return birthHour;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public class UserService {
|
|||||||
profile.setUserId(userId);
|
profile.setUserId(userId);
|
||||||
profile.setGender(gender);
|
profile.setGender(gender);
|
||||||
profile.setBirthday(request.getBirthday());
|
profile.setBirthday(request.getBirthday());
|
||||||
|
profile.setBirthdayCalendar(request.getBirthdayCalendar());
|
||||||
profile.setBirthShichen(shichen);
|
profile.setBirthShichen(shichen);
|
||||||
profile.setResidenceProvince(request.getProvince());
|
profile.setResidenceProvince(request.getProvince());
|
||||||
profile.setResidenceCity(request.getCity());
|
profile.setResidenceCity(request.getCity());
|
||||||
@@ -124,6 +125,7 @@ public class UserService {
|
|||||||
fileAccessService.resolveUrl(user.getAvatarUrl()),
|
fileAccessService.resolveUrl(user.getAvatarUrl()),
|
||||||
user.getGender(),
|
user.getGender(),
|
||||||
user.getBirthday(),
|
user.getBirthday(),
|
||||||
|
"lunar".equals(user.getBirthdayCalendar()) ? "lunar" : "solar",
|
||||||
user.getBirthHour(),
|
user.getBirthHour(),
|
||||||
user.getProvince(),
|
user.getProvince(),
|
||||||
user.getCity(),
|
user.getCity(),
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 在部署新版后端前,由部署人员在目标数据库执行。不在应用启动时修改远程库。
|
||||||
|
-- 仅增加显示偏好列,不重写生日,不删表或删除数据。MySQL DDL 会隐式提交。
|
||||||
|
SET @birthday_calendar_ddl = IF((SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'user_profile' AND column_name = 'birthday_calendar') = 0,
|
||||||
|
'ALTER TABLE user_profile ADD COLUMN birthday_calendar VARCHAR(8) NOT NULL DEFAULT ''solar'' COMMENT ''生日显示历法 solar阳历 lunar农历,birthday始终为阳历日期''',
|
||||||
|
'DO 0');
|
||||||
|
PREPARE birthday_calendar_stmt FROM @birthday_calendar_ddl;
|
||||||
|
EXECUTE birthday_calendar_stmt;
|
||||||
|
DEALLOCATE PREPARE birthday_calendar_stmt;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- 部署前在目标数据库手工执行,不修改历史记录。
|
||||||
|
-- 不删除功德历史列或流水,本期在业务接口禁止功德行为。
|
||||||
|
ALTER TABLE user_virtual_item_transfer
|
||||||
|
MODIFY COLUMN to_user_id BIGINT UNSIGNED NULL COMMENT '注册接收方账号,可为空表示仅按姓名回向';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?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.nfc.TiangongMapper">
|
||||||
|
<select id="findByUser" resultType="com.hongzhen.hera.nfc.TiangongItemView">
|
||||||
|
SELECT v.id, v.item_name AS name, v.icon_url, v.description, h.id AS holding_id, h.obtained_at,
|
||||||
|
CASE WHEN h.status = 1 AND EXISTS (SELECT c.id FROM nfc_chip c
|
||||||
|
WHERE c.current_bind_user_id = #{userId} AND c.virtual_item_id = v.id) THEN 1 ELSE 0 END AS active,
|
||||||
|
(SELECT COUNT(o.id) FROM user_virtual_item_offering o WHERE o.user_id = #{userId}
|
||||||
|
AND o.user_virtual_item_id = h.id AND o.offering_type = 2 AND o.status = 1) AS incense_count
|
||||||
|
FROM virtual_item v LEFT JOIN user_virtual_item h ON h.virtual_item_id = v.id AND h.user_id = #{userId}
|
||||||
|
WHERE v.enabled = 1 ORDER BY v.sort_order, v.id
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
a.id, a.wechat_openid AS open_id, a.wechat_unionid AS union_id, a.mobile AS phone,
|
a.id, a.wechat_openid AS open_id, a.wechat_unionid AS union_id, a.mobile AS phone,
|
||||||
a.nickname, a.status, a.created_at, p.updated_at, p.avatar_url,
|
a.nickname, a.status, a.created_at, p.updated_at, p.avatar_url,
|
||||||
CASE p.gender WHEN 1 THEN '男' WHEN 2 THEN '女' ELSE '未知' END AS gender,
|
CASE p.gender WHEN 1 THEN '男' WHEN 2 THEN '女' ELSE '未知' END AS gender,
|
||||||
p.birthday,
|
p.birthday, COALESCE(p.birthday_calendar, 'solar') AS birthday_calendar,
|
||||||
ELT(p.birth_shichen + 1, '子时', '丑时', '寅时', '卯时', '辰时', '巳时',
|
ELT(p.birth_shichen + 1, '子时', '丑时', '寅时', '卯时', '辰时', '巳时',
|
||||||
'午时', '未时', '申时', '酉时', '戌时', '亥时') AS birth_hour,
|
'午时', '未时', '申时', '酉时', '戌时', '亥时') AS birth_hour,
|
||||||
p.residence_province AS province, p.residence_city AS city, p.residence_district AS district,
|
p.residence_province AS province, p.residence_city AS city, p.residence_district AS district,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<mapper namespace="com.hongzhen.hera.data.UserProfileMapper">
|
<mapper namespace="com.hongzhen.hera.data.UserProfileMapper">
|
||||||
|
|
||||||
<sql id="BaseColumnList">
|
<sql id="BaseColumnList">
|
||||||
id, user_id, real_name_encrypted, gender, birthday, birth_shichen, birth_country_code,
|
id, user_id, real_name_encrypted, gender, birthday, birthday_calendar, birth_shichen, birth_country_code,
|
||||||
birth_region_code, birth_city_code, birth_district_code, id_card_no_encrypted, id_card_hash,
|
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,
|
id_verified, id_verified_at, avatar_url, residence_province, residence_city, residence_district,
|
||||||
residence_address, signature, profile_edit_count,
|
residence_address, signature, profile_edit_count,
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="updatePersonal">
|
<update id="updatePersonal">
|
||||||
UPDATE user_profile SET gender = #{gender}, birthday = #{birthday}, birth_shichen = #{birthShichen},
|
UPDATE user_profile SET gender = #{gender}, birthday = #{birthday}, birthday_calendar = #{birthdayCalendar}, birth_shichen = #{birthShichen},
|
||||||
residence_province = #{residenceProvince}, residence_city = #{residenceCity},
|
residence_province = #{residenceProvince}, residence_city = #{residenceCity},
|
||||||
residence_district = #{residenceDistrict}, profile_edit_count = #{profileEditCount},
|
residence_district = #{residenceDistrict}, profile_edit_count = #{profileEditCount},
|
||||||
profile_edit_window_start = #{profileEditWindowStart}
|
profile_edit_window_start = #{profileEditWindowStart}
|
||||||
@@ -41,12 +41,12 @@
|
|||||||
|
|
||||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
INSERT INTO user_profile (
|
INSERT INTO user_profile (
|
||||||
user_id, real_name_encrypted, gender, birthday, birth_shichen, birth_country_code,
|
user_id, real_name_encrypted, gender, birthday, birthday_calendar, birth_shichen, birth_country_code,
|
||||||
birth_region_code, birth_city_code, birth_district_code, id_card_no_encrypted, id_card_hash,
|
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,
|
id_verified, id_verified_at, avatar_url, residence_province, residence_city, residence_district,
|
||||||
residence_address, signature
|
residence_address, signature
|
||||||
) VALUES (
|
) VALUES (
|
||||||
#{userId}, #{realNameEncrypted}, COALESCE(#{gender}, 0), #{birthday}, #{birthShichen},
|
#{userId}, #{realNameEncrypted}, COALESCE(#{gender}, 0), #{birthday}, COALESCE(#{birthdayCalendar}, 'solar'), #{birthShichen},
|
||||||
#{birthCountryCode}, #{birthRegionCode}, #{birthCityCode}, #{birthDistrictCode},
|
#{birthCountryCode}, #{birthRegionCode}, #{birthCityCode}, #{birthDistrictCode},
|
||||||
#{idCardNoEncrypted}, #{idCardHash}, COALESCE(#{idVerified}, 0), #{idVerifiedAt}, #{avatarUrl},
|
#{idCardNoEncrypted}, #{idCardHash}, COALESCE(#{idVerified}, 0), #{idVerifiedAt}, #{avatarUrl},
|
||||||
#{residenceProvince}, #{residenceCity}, #{residenceDistrict}, #{residenceAddress}, #{signature}
|
#{residenceProvince}, #{residenceCity}, #{residenceDistrict}, #{residenceAddress}, #{signature}
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
real_name_encrypted = #{realNameEncrypted},
|
real_name_encrypted = #{realNameEncrypted},
|
||||||
gender = COALESCE(#{gender}, gender),
|
gender = COALESCE(#{gender}, gender),
|
||||||
birthday = #{birthday},
|
birthday = #{birthday},
|
||||||
|
birthday_calendar = COALESCE(#{birthdayCalendar}, birthday_calendar),
|
||||||
birth_shichen = #{birthShichen},
|
birth_shichen = #{birthShichen},
|
||||||
birth_country_code = #{birthCountryCode},
|
birth_country_code = #{birthCountryCode},
|
||||||
birth_region_code = #{birthRegionCode},
|
birth_region_code = #{birthRegionCode},
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ CREATE TABLE IF NOT EXISTS user_profile (
|
|||||||
real_name_encrypted VARCHAR(128) NULL,
|
real_name_encrypted VARCHAR(128) NULL,
|
||||||
gender TINYINT NOT NULL DEFAULT 0,
|
gender TINYINT NOT NULL DEFAULT 0,
|
||||||
birthday DATE NULL,
|
birthday DATE NULL,
|
||||||
|
birthday_calendar VARCHAR(8) NOT NULL DEFAULT 'solar' COMMENT '生日显示历法 solar阳历 lunar农历,birthday始终为阳历日期',
|
||||||
birth_shichen TINYINT UNSIGNED NULL,
|
birth_shichen TINYINT UNSIGNED NULL,
|
||||||
birth_country_code CHAR(2) NULL,
|
birth_country_code CHAR(2) NULL,
|
||||||
birth_region_code VARCHAR(20) NULL,
|
birth_region_code VARCHAR(20) NULL,
|
||||||
@@ -168,7 +169,7 @@ CREATE TABLE IF NOT EXISTS user_virtual_item_transfer (
|
|||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
from_user_id BIGINT UNSIGNED NOT NULL,
|
from_user_id BIGINT UNSIGNED NOT NULL,
|
||||||
from_user_name VARCHAR(50) NULL,
|
from_user_name VARCHAR(50) NULL,
|
||||||
to_user_id BIGINT UNSIGNED NOT NULL,
|
to_user_id BIGINT UNSIGNED NULL COMMENT '注册接收方账号,可为空表示仅按姓名回向',
|
||||||
to_user_name VARCHAR(50) NULL,
|
to_user_name VARCHAR(50) NULL,
|
||||||
virtual_item_id TINYINT UNSIGNED NOT NULL,
|
virtual_item_id TINYINT UNSIGNED NOT NULL,
|
||||||
from_user_item_id BIGINT UNSIGNED NULL,
|
from_user_item_id BIGINT UNSIGNED NULL,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class NfcMySqlIntegrationTest {
|
|||||||
UserVirtualItemTransferMapper transfers = session.getMapper(UserVirtualItemTransferMapper.class);
|
UserVirtualItemTransferMapper transfers = session.getMapper(UserVirtualItemTransferMapper.class);
|
||||||
UserNfcBindingMapper bindings = session.getMapper(UserNfcBindingMapper.class);
|
UserNfcBindingMapper bindings = session.getMapper(UserNfcBindingMapper.class);
|
||||||
VirtualAssetService assets = new VirtualAssetService(session.getMapper(UserAccountMapper.class),
|
VirtualAssetService assets = new VirtualAssetService(session.getMapper(UserAccountMapper.class),
|
||||||
session.getMapper(VirtualItemMapper.class), holdings, offerings, transfers, chips);
|
session.getMapper(VirtualItemMapper.class), holdings, offerings, transfers, chips, session.getMapper(TiangongMapper.class));
|
||||||
NfcService nfc = new NfcService(chips, bindings, assets);
|
NfcService nfc = new NfcService(chips, bindings, assets);
|
||||||
try (Statement statement = connection.createStatement()) {
|
try (Statement statement = connection.createStatement()) {
|
||||||
statement.executeUpdate("INSERT INTO user_account(id, username, nickname, mobile, wechat_openid) "
|
statement.executeUpdate("INSERT INTO user_account(id, username, nickname, mobile, wechat_openid) "
|
||||||
@@ -129,8 +129,7 @@ class NfcMySqlIntegrationTest {
|
|||||||
long holdingId = holdings.findByUserAndItem(1L, 1).get().getId();
|
long holdingId = holdings.findByUserAndItem(1L, 1).get().getId();
|
||||||
NfcDtos.OfferingRequest offer = new NfcDtos.OfferingRequest();
|
NfcDtos.OfferingRequest offer = new NfcDtos.OfferingRequest();
|
||||||
offer.setUserVirtualItemId(holdingId);
|
offer.setUserVirtualItemId(holdingId);
|
||||||
offer.setOfferingType(OfferingType.MERIT.getCode());
|
offer.setOfferingType(OfferingType.INCENSE.getCode());
|
||||||
offer.setMeritValue(8);
|
|
||||||
assets.offer(1L, offer);
|
assets.offer(1L, offer);
|
||||||
assertEquals(2, offerings.findByUser(1L, holdingId, 0, 100).size());
|
assertEquals(2, offerings.findByUser(1L, holdingId, 0, 100).size());
|
||||||
NfcDtos.TransferRequest dedication = new NfcDtos.TransferRequest();
|
NfcDtos.TransferRequest dedication = new NfcDtos.TransferRequest();
|
||||||
|
|||||||
@@ -200,19 +200,69 @@ class NfcWorkflowTest {
|
|||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, 2, ",\"meritValue\":10"), 400);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 2, ",\"meritValue\":10"), 400);
|
||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, 4, ""), 400);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 4, ""), 400);
|
||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, 4, ",\"meritValue\":-1"), 400);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 4, ",\"meritValue\":-1"), 400);
|
||||||
for (int type : Arrays.asList(2, 3, 4, 5)) {
|
for (int type : Arrays.asList(2, 3, 5)) {
|
||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, type, type == 4 ? ",\"meritValue\":12" : ""), 200);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, type, type == 3 ? ",\"content\":\"愿家人平安\"" : ""), 200);
|
||||||
}
|
}
|
||||||
assertEquals(0, integer("SELECT status FROM user_virtual_item WHERE id = " + id));
|
assertEquals(0, integer("SELECT status FROM user_virtual_item WHERE id = " + id));
|
||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, 2, ""), 400);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 2, ""), 400);
|
||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, 1, ""), 200);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 1, ""), 200);
|
||||||
postJson(1, "/api/user/virtual-items/offerings", offering(id, 1, ""), 400);
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 1, ""), 400);
|
||||||
assertEquals(1, integer("SELECT status FROM user_virtual_item WHERE id = " + id));
|
assertEquals(1, integer("SELECT status FROM user_virtual_item WHERE id = " + id));
|
||||||
assertEquals(12, integer("SELECT merit_value FROM user_virtual_item_offering WHERE offering_type = 4"));
|
assertEquals(0, integer("SELECT COUNT(id) FROM user_virtual_item_offering WHERE offering_type = 4"));
|
||||||
assertEquals(6, getJson(1, "/api/user/virtual-items/offerings?holdingId=" + id).size());
|
assertEquals(5, getJson(1, "/api/user/virtual-items/offerings?holdingId=" + id).size());
|
||||||
assertEquals(0, getJson(2, "/api/user/virtual-items/offerings").size());
|
assertEquals(0, getJson(2, "/api/user/virtual-items/offerings").size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tiangongAggregatesConfirmedIncenseAndNeverExposesMeritOrForeignOwnership() throws Exception {
|
||||||
|
nfc.bind(1L, "chip-a");
|
||||||
|
long id = holding(1, 1);
|
||||||
|
for (int count = 0; count < 3; count++) {
|
||||||
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 2, ""), 200);
|
||||||
|
}
|
||||||
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 3, ""), 400);
|
||||||
|
postJson(1, "/api/user/virtual-items/offerings", offering(id, 4, ",\"meritValue\":8"), 400);
|
||||||
|
JsonNode list = getJson(1, "/api/tiangong/items");
|
||||||
|
assertEquals(2, list.size());
|
||||||
|
JsonNode wealth = list.get(1);
|
||||||
|
assertEquals("财神", wealth.path("name").asText());
|
||||||
|
assertEquals(id, wealth.path("holdingId").asLong());
|
||||||
|
assertTrue(wealth.path("active").asBoolean());
|
||||||
|
assertEquals(3, wealth.path("incenseCount").asInt());
|
||||||
|
assertFalse(wealth.has("merit"));
|
||||||
|
assertFalse(wealth.has("balance"));
|
||||||
|
JsonNode foreign = getJson(2, "/api/tiangong/items").get(1);
|
||||||
|
assertFalse(foreign.path("active").asBoolean());
|
||||||
|
assertTrue(foreign.path("holdingId").isNull());
|
||||||
|
assertEquals(0, foreign.path("incenseCount").asInt());
|
||||||
|
nfc.unbind(1L, "chip-a");
|
||||||
|
assertFalse(getJson(1, "/api/tiangong/items").get(1).path("active").asBoolean());
|
||||||
|
assertEquals(3, integer("SELECT COUNT(id) FROM user_virtual_item_offering WHERE offering_type = 2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dedicationToArbitraryNameIsPersistedWithoutRecipientAccountOrAssetMutation() throws Exception {
|
||||||
|
nfc.bind(1L, "chip-a");
|
||||||
|
long id = holding(1, 1);
|
||||||
|
String body = "{\"userVirtualItemId\":" + id + ",\"name\":\" 家人 \",\"content\":\" 愿平安顺遂 \"}";
|
||||||
|
JsonNode record = postJson(1, "/api/user/virtual-items/dedications", body, 200);
|
||||||
|
assertEquals("家人", record.path("toUserName").asText());
|
||||||
|
assertEquals("愿平安顺遂", record.path("content").asText());
|
||||||
|
assertTrue(record.path("toUserId").isNull());
|
||||||
|
assertTrue(record.path("toUserItemId").isNull());
|
||||||
|
assertEquals(1, integer("SELECT COUNT(id) FROM user_virtual_item_transfer WHERE to_user_id IS NULL"));
|
||||||
|
assertEquals(3, integer("SELECT COUNT(id) FROM user_account"));
|
||||||
|
assertEquals(1, integer("SELECT COUNT(id) FROM user_virtual_item WHERE status = 1"));
|
||||||
|
assertEquals(1, getJson(1, "/api/user/virtual-items/transfers").size());
|
||||||
|
assertEquals(0, getJson(2, "/api/user/virtual-items/transfers").size());
|
||||||
|
postJson(2, "/api/user/virtual-items/dedications", body, 400);
|
||||||
|
postJson(1, "/api/user/virtual-items/dedications", body.replace(" 家人 ", " "), 400);
|
||||||
|
postJson(1, "/api/user/virtual-items/dedications", body.replace(" 愿平安顺遂 ", " "), 400);
|
||||||
|
nfc.unbind(1L, "chip-a");
|
||||||
|
postJson(1, "/api/user/virtual-items/dedications", body, 400);
|
||||||
|
assertEquals(1, integer("SELECT COUNT(id) FROM user_virtual_item_transfer"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void dedicationRecordsSnapshotsWithoutTransferringOrCreatingAssets() throws Exception {
|
void dedicationRecordsSnapshotsWithoutTransferringOrCreatingAssets() throws Exception {
|
||||||
nfc.bind(1L, "chip-a");
|
nfc.bind(1L, "chip-a");
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.hongzhen.hera.auth.WechatClient;
|
|||||||
import com.hongzhen.hera.auth.WxSession;
|
import com.hongzhen.hera.auth.WxSession;
|
||||||
import com.hongzhen.hera.data.UserAccount;
|
import com.hongzhen.hera.data.UserAccount;
|
||||||
import com.hongzhen.hera.data.UserAccountMapper;
|
import com.hongzhen.hera.data.UserAccountMapper;
|
||||||
|
import com.hongzhen.hera.data.UserProfile;
|
||||||
|
import com.hongzhen.hera.data.UserProfileMapper;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -44,6 +46,7 @@ public class UserUnificationTest {
|
|||||||
@Autowired private ObjectMapper json;
|
@Autowired private ObjectMapper json;
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
@Autowired private UserAccountMapper accountMapper;
|
@Autowired private UserAccountMapper accountMapper;
|
||||||
|
@Autowired private UserProfileMapper profileMapper;
|
||||||
@Autowired private ApplicationContext context;
|
@Autowired private ApplicationContext context;
|
||||||
@MockBean private WechatClient wechatClient;
|
@MockBean private WechatClient wechatClient;
|
||||||
private String openId;
|
private String openId;
|
||||||
@@ -179,6 +182,57 @@ public class UserUnificationTest {
|
|||||||
+ "\",\"phone\":\"" + phone + "\",\"captcha\":\"" + sms.path("mockCode").asText() + "\"}");
|
+ "\",\"phone\":\"" + phone + "\",\"captcha\":\"" + sms.path("mockCode").asText() + "\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void birthdayCalendarPersistsAcrossProfileAndRepeatLoginWithoutChangingSolarBirthday() throws Exception {
|
||||||
|
JsonNode login = register("13800000007");
|
||||||
|
long id = login.path("user").path("id").asLong();
|
||||||
|
String token = login.path("token").asText();
|
||||||
|
assertEquals("solar", login.path("user").path("birthdayCalendar").asText());
|
||||||
|
String lunarBody = profileBody("农历生日").replace("\"birthday\":", "\"birthdayCalendar\":\"lunar\",\"birthday\":");
|
||||||
|
mvc.perform(put("/api/user/profile").header("Authorization", "Bearer " + token)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content(lunarBody)).andExpect(status().isOk());
|
||||||
|
assertEquals("lunar", jdbc.queryForObject("SELECT birthday_calendar FROM user_profile WHERE user_id = ?", String.class, id));
|
||||||
|
String body = mvc.perform(get("/api/user/profile").header("Authorization", "Bearer " + token))
|
||||||
|
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
JsonNode profile = json.readTree(body).path("data");
|
||||||
|
assertEquals("lunar", profile.path("birthdayCalendar").asText());
|
||||||
|
assertEquals("1998-06-12", profile.path("birthday").asText());
|
||||||
|
JsonNode again = post("/api/auth/wx-login", "{\"code\":\"repeat-calendar\"}");
|
||||||
|
assertEquals("lunar", again.path("user").path("birthdayCalendar").asText());
|
||||||
|
mvc.perform(put("/api/user/profile").header("Authorization", "Bearer " + token)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content(lunarBody.replace("\"lunar\"", "\"solar\"")))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
assertEquals("solar", jdbc.queryForObject("SELECT birthday_calendar FROM user_profile WHERE user_id = ?", String.class, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidBirthdayCalendarDoesNotConsumeProfileQuota() throws Exception {
|
||||||
|
JsonNode login = register("13800000008");
|
||||||
|
String token = login.path("token").asText();
|
||||||
|
String invalid = profileBody("非法历法").replace("\"birthday\":", "\"birthdayCalendar\":\"invalid\",\"birthday\":");
|
||||||
|
mvc.perform(put("/api/user/profile").header("Authorization", "Bearer " + token)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content(invalid)).andExpect(status().isBadRequest());
|
||||||
|
assertEquals(0, jdbc.queryForObject("SELECT profile_edit_count FROM user_profile WHERE user_id = ?", Integer.class, login.path("user").path("id").asLong()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void profileCrudMapperReadsAndWritesBirthdayCalendar() {
|
||||||
|
UserAccount account = new UserAccount();
|
||||||
|
account.setUsername("calendar_" + UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
accountMapper.insert(account);
|
||||||
|
UserProfile profile = new UserProfile();
|
||||||
|
profile.setUserId(account.getId());
|
||||||
|
profile.setBirthday(java.time.LocalDate.of(2023, 3, 22));
|
||||||
|
profile.setBirthdayCalendar("lunar");
|
||||||
|
assertEquals(1, profileMapper.insert(profile));
|
||||||
|
UserProfile stored = profileMapper.findById(profile.getId()).get();
|
||||||
|
assertEquals("lunar", stored.getBirthdayCalendar());
|
||||||
|
stored.setBirthdayCalendar("solar");
|
||||||
|
assertEquals(1, profileMapper.update(stored));
|
||||||
|
assertEquals("solar", profileMapper.findById(profile.getId()).get().getBirthdayCalendar());
|
||||||
|
assertEquals(profile.getBirthday(), profileMapper.findById(profile.getId()).get().getBirthday());
|
||||||
|
}
|
||||||
|
|
||||||
private JsonNode post(String path, String body) throws Exception {
|
private JsonNode post(String path, String body) throws Exception {
|
||||||
String response = mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post(path)
|
String response = mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post(path)
|
||||||
.contentType(MediaType.APPLICATION_JSON).content(body))
|
.contentType(MediaType.APPLICATION_JSON).content(body))
|
||||||
|
|||||||
Reference in new issue
Block a user