feat: add hera backend

This commit is contained in:
zhouzhibo committed 2026-10-13 22:53:52 +08:00
1 parent 5b906bf8f3
commit 4222b7124c
34 files changed
+2023 -83

No files matched your search

@@ -0,0 +1,12 @@
package com.hongzhen.hera;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HeraApplication {
public static void main(String[] args) {
SpringApplication.run(HeraApplication.class, args);
}
}
@@ -0,0 +1,46 @@
package com.hongzhen.hera.auth;
import com.hongzhen.hera.auth.AuthDtos.BindPhoneRequest;
import com.hongzhen.hera.auth.AuthDtos.LoginResponse;
import com.hongzhen.hera.auth.AuthDtos.SendSmsRequest;
import com.hongzhen.hera.auth.AuthDtos.WxLoginRequest;
import com.hongzhen.hera.auth.AuthDtos.WxLoginResponse;
import com.hongzhen.hera.common.ApiResponse;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
private final SmsService smsService;
public AuthController(AuthService authService, SmsService smsService) {
this.authService = authService;
this.smsService = smsService;
}
@PostMapping("/wx-login")
public ApiResponse<WxLoginResponse> wxLogin(@Valid @RequestBody WxLoginRequest request) {
return ApiResponse.ok(authService.wxLogin(request.getCode()));
}
@PostMapping("/sms")
public ApiResponse<Map<String, String>> sendSms(@Valid @RequestBody SendSmsRequest request) {
String code = smsService.send(request.getPhone());
Map<String, String> data = new HashMap<String, String>();
data.put("mockCode", code);
return ApiResponse.ok(data);
}
@PostMapping("/bind-phone")
public ApiResponse<LoginResponse> bindPhone(@Valid @RequestBody BindPhoneRequest request) {
return ApiResponse.ok(authService.bindPhone(request));
}
}
@@ -0,0 +1,83 @@
package com.hongzhen.hera.auth;
import com.hongzhen.hera.user.ProfileDtos.ProfileResponse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class AuthDtos {
public static class WxLoginRequest {
@NotBlank
private String code;
public String getCode() {
return code;
}
}
public static class WxLoginResponse {
private final String sessionKey;
private final boolean phoneBound;
public WxLoginResponse(String sessionKey, boolean phoneBound) {
this.sessionKey = sessionKey;
this.phoneBound = phoneBound;
}
public String getSessionKey() {
return sessionKey;
}
public boolean isPhoneBound() {
return phoneBound;
}
}
public static class SendSmsRequest {
@Pattern(regexp = "^1\\d{10}$")
private String phone;
public String getPhone() {
return phone;
}
}
public static class BindPhoneRequest {
@NotBlank
private String sessionKey;
@Pattern(regexp = "^1\\d{10}$")
private String phone;
@NotBlank
private String captcha;
public String getSessionKey() {
return sessionKey;
}
public String getPhone() {
return phone;
}
public String getCaptcha() {
return captcha;
}
}
public static class LoginResponse {
private final String token;
private final ProfileResponse user;
public LoginResponse(String token, ProfileResponse user) {
this.token = token;
this.user = user;
}
public String getToken() {
return token;
}
public ProfileResponse getUser() {
return user;
}
}
}
@@ -0,0 +1,72 @@
package com.hongzhen.hera.auth;
import com.hongzhen.hera.auth.AuthDtos.BindPhoneRequest;
import com.hongzhen.hera.auth.AuthDtos.LoginResponse;
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.UserService;
import java.time.LocalDateTime;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AuthService {
private final WechatClient wechatClient;
private final LoginSessionRepository loginSessionRepository;
private final UserRepository userRepository;
private final SmsService smsService;
private final JwtService jwtService;
private final UserService userService;
public AuthService(
WechatClient wechatClient,
LoginSessionRepository loginSessionRepository,
UserRepository userRepository,
SmsService smsService,
JwtService jwtService,
UserService userService
) {
this.wechatClient = wechatClient;
this.loginSessionRepository = loginSessionRepository;
this.userRepository = userRepository;
this.smsService = smsService;
this.jwtService = jwtService;
this.userService = userService;
}
@Transactional
public WxLoginResponse wxLogin(String code) {
WxSession wxSession = wechatClient.code2Session(code);
String sessionKey = UUID.randomUUID().toString();
LoginSession loginSession = new LoginSession();
loginSession.setSessionKey(sessionKey);
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);
return new WxLoginResponse(sessionKey, phoneBound);
}
@Transactional
public LoginResponse bindPhone(BindPhoneRequest request) {
smsService.verify(request.getPhone(), request.getCaptcha());
LoginSession session = loginSessionRepository.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.setOpenId(session.getOpenId());
user.setUnionId(session.getUnionId());
user.setPhone(request.getPhone());
user.touch();
userRepository.save(user);
return new LoginResponse(jwtService.issue(user.getId()), userService.toProfile(user));
}
}
@@ -0,0 +1,61 @@
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)
private String openId;
private String unionId;
private LocalDateTime expiresAt;
public Long getId() {
return id;
}
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getUnionId() {
return unionId;
}
public void setUnionId(String unionId) {
this.unionId = unionId;
}
public LocalDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(LocalDateTime expiresAt) {
this.expiresAt = expiresAt;
}
}
@@ -0,0 +1,9 @@
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);
}
@@ -0,0 +1,52 @@
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 String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(LocalDateTime expiresAt) {
this.expiresAt = expiresAt;
}
public boolean isUsed() {
return used;
}
public void setUsed(boolean used) {
this.used = used;
}
}
@@ -0,0 +1,9 @@
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);
}
@@ -0,0 +1,39 @@
package com.hongzhen.hera.auth;
import com.hongzhen.hera.common.BusinessException;
import java.time.LocalDateTime;
import java.util.Random;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SmsService {
private final SmsCodeRepository smsCodeRepository;
private final Random random = new Random();
public SmsService(SmsCodeRepository smsCodeRepository) {
this.smsCodeRepository = smsCodeRepository;
}
@Transactional
public String send(String phone) {
String code = String.format("%06d", random.nextInt(1_000_000));
SmsCode smsCode = new SmsCode();
smsCode.setPhone(phone);
smsCode.setCode(code);
smsCode.setExpiresAt(LocalDateTime.now().plusMinutes(5));
smsCodeRepository.save(smsCode);
return code;
}
@Transactional
public void verify(String phone, String code) {
SmsCode smsCode = smsCodeRepository.findTopByPhoneAndCodeAndUsedFalseOrderByExpiresAtDesc(phone, code)
.orElseThrow(() -> new BusinessException("验证码不正确"));
if (smsCode.getExpiresAt().isBefore(LocalDateTime.now())) {
throw new BusinessException("验证码已过期");
}
smsCode.setUsed(true);
}
}
@@ -0,0 +1,71 @@
package com.hongzhen.hera.auth;
import com.hongzhen.hera.config.HeraProperties;
import com.hongzhen.hera.common.BusinessException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Component
public class WechatClient {
private final HeraProperties properties;
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
public WechatClient(HeraProperties properties) {
this.properties = properties;
}
public WxSession code2Session(String code) {
if (properties.getWx().isMock()) {
return new WxSession("mock-openid-" + sha1(code).substring(0, 12), null);
}
if (properties.getWx().getAppSecret() == null || properties.getWx().getAppSecret().trim().isEmpty()) {
throw new BusinessException("未配置微信小程序 secret");
}
String url = UriComponentsBuilder
.fromHttpUrl("https://api.weixin.qq.com/sns/jscode2session")
.queryParam("appid", properties.getWx().getAppId())
.queryParam("secret", properties.getWx().getAppSecret())
.queryParam("js_code", code)
.queryParam("grant_type", "authorization_code")
.toUriString();
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
try {
JsonNode body = objectMapper.readTree(response.getBody());
if (body.has("errcode") && body.get("errcode").asInt() != 0) {
throw new BusinessException("微信登录失败:" + body.path("errmsg").asText());
}
String openId = body.path("openid").asText();
if (openId == null || openId.trim().isEmpty()) {
throw new BusinessException("微信登录失败:未返回 openid");
}
String unionId = body.has("unionid") ? body.path("unionid").asText(null) : null;
return new WxSession(openId, unionId);
} catch (IOException exception) {
throw new BusinessException("微信登录响应解析失败");
}
}
private String sha1(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.append(String.format("%02x", b));
}
return builder.toString();
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException(exception);
}
}
}
@@ -0,0 +1,20 @@
package com.hongzhen.hera.auth;
public class WxSession {
private final String openId;
private final String unionId;
public WxSession(String openId, String unionId) {
this.openId = openId;
this.unionId = unionId;
}
public String getOpenId() {
return openId;
}
public String getUnionId() {
return unionId;
}
}
@@ -0,0 +1,34 @@
package com.hongzhen.hera.common;
public class ApiResponse<T> {
private final int code;
private final String message;
private final T data;
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<T>(0, "ok", data);
}
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<T>(400, message, null);
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
}
@@ -0,0 +1,8 @@
package com.hongzhen.hera.common;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
@@ -0,0 +1,24 @@
package com.hongzhen.hera.common;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleBusiness(BusinessException exception) {
return ApiResponse.fail(exception.getMessage());
}
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleValidation(Exception exception) {
return ApiResponse.fail("参数不正确");
}
}
@@ -0,0 +1,69 @@
package com.hongzhen.hera.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "hera")
public class HeraProperties {
private String jwtSecret;
private final Wx wx = new Wx();
private final Sms sms = new Sms();
public String getJwtSecret() {
return jwtSecret;
}
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
public Wx getWx() {
return wx;
}
public Sms getSms() {
return sms;
}
public static class Wx {
private String appId;
private String appSecret;
private boolean mock = true;
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public boolean isMock() {
return mock;
}
public void setMock(boolean mock) {
this.mock = mock;
}
}
public static class Sms {
private boolean mock = true;
public boolean isMock() {
return mock;
}
public void setMock(boolean mock) {
this.mock = mock;
}
}
}
@@ -0,0 +1,31 @@
package com.hongzhen.hera.config;
import com.hongzhen.hera.security.AuthInterceptor;
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.WebMvcConfigurer;
@Configuration
@EnableConfigurationProperties(HeraProperties.class)
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("*").allowedOrigins("*");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**", "/api/content/**", "/h2-console/**");
}
}
@@ -0,0 +1,34 @@
package com.hongzhen.hera.content;
import com.hongzhen.hera.common.ApiResponse;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/content")
public class ContentController {
@GetMapping("/agreement/{type}")
public ApiResponse<Map<String, String>> agreement(@PathVariable String type) {
String title = "privacy".equals(type) ? "隐私政策" : "用户协议";
String body = "欢迎使用那吉。本页面内容为占位协议文本,请在正式上线前替换为经法务确认的完整条款。";
Map<String, String> data = new HashMap<String, String>();
data.put("title", title);
data.put("body", body);
return ApiResponse.ok(data);
}
@GetMapping("/follow")
public ApiResponse<Map<String, String>> follow() {
Map<String, String> data = new HashMap<String, String>();
data.put("title", "关注我们 一触即吉");
data.put("redBookKeyword", "那小吉");
data.put("videoKeyword", "那吉 nagiflow");
data.put("brand", "那吉 NAGI FLOW");
return ApiResponse.ok(data);
}
}
@@ -0,0 +1,32 @@
package com.hongzhen.hera.daily;
import com.hongzhen.hera.common.ApiResponse;
import com.hongzhen.hera.daily.DailyDtos.DailyHomeResponse;
import com.hongzhen.hera.daily.DailyDtos.PinCardResponse;
import com.hongzhen.hera.security.CurrentUser;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/daily")
public class DailyController {
private final DailyService dailyService;
public DailyController(DailyService dailyService) {
this.dailyService = dailyService;
}
@GetMapping("/home")
public ApiResponse<DailyHomeResponse> home() {
return ApiResponse.ok(dailyService.home(CurrentUser.id()));
}
@PostMapping("/cards/{cardId}/pin")
public ApiResponse<PinCardResponse> pin(@PathVariable String cardId) {
return ApiResponse.ok(new PinCardResponse(dailyService.pin(CurrentUser.id(), cardId)));
}
}
@@ -0,0 +1,256 @@
package com.hongzhen.hera.daily;
import java.time.LocalDate;
import java.util.List;
public class DailyDtos {
public static class DailyHomeResponse {
private LocalDate date;
private String lunarDate;
private String weekName;
private String fortuneLevel;
private String greeting;
private String dailySummary;
private List<CalendarDay> calendarDays;
private List<LuckyPanel> luckyPanels;
private List<FortuneCard> cards;
private List<LifeTip> lifeTips;
private String pinnedCardId;
public DailyHomeResponse(LocalDate date, String lunarDate, String weekName, String fortuneLevel, String greeting,
String dailySummary, List<CalendarDay> calendarDays, List<LuckyPanel> luckyPanels, List<FortuneCard> cards,
List<LifeTip> lifeTips, String pinnedCardId) {
this.date = date;
this.lunarDate = lunarDate;
this.weekName = weekName;
this.fortuneLevel = fortuneLevel;
this.greeting = greeting;
this.dailySummary = dailySummary;
this.calendarDays = calendarDays;
this.luckyPanels = luckyPanels;
this.cards = cards;
this.lifeTips = lifeTips;
this.pinnedCardId = pinnedCardId;
}
public LocalDate getDate() {
return date;
}
public String getLunarDate() {
return lunarDate;
}
public String getWeekName() {
return weekName;
}
public String getFortuneLevel() {
return fortuneLevel;
}
public String getGreeting() {
return greeting;
}
public String getDailySummary() {
return dailySummary;
}
public List<CalendarDay> getCalendarDays() {
return calendarDays;
}
public List<LuckyPanel> getLuckyPanels() {
return luckyPanels;
}
public List<FortuneCard> getCards() {
return cards;
}
public List<LifeTip> getLifeTips() {
return lifeTips;
}
public String getPinnedCardId() {
return pinnedCardId;
}
}
public static class CalendarDay {
private int day;
private String weekName;
private String label;
public CalendarDay(int day, String weekName, String label) {
this.day = day;
this.weekName = weekName;
this.label = label;
}
public int getDay() {
return day;
}
public String getWeekName() {
return weekName;
}
public String getLabel() {
return label;
}
}
public static class LuckyPanel {
private String title;
private String value;
private String accent;
public LuckyPanel(String title, String value, String accent) {
this.title = title;
this.value = value;
this.accent = accent;
}
public String getTitle() {
return title;
}
public String getValue() {
return value;
}
public String getAccent() {
return accent;
}
}
public static class FortuneCard {
private String id;
private String title;
private String category;
private String categoryIcon;
private String verdict;
private String avoid;
private String summary;
private String detail;
private String color;
private String gradient;
private String mascot;
public FortuneCard(String id, String title, String category, String categoryIcon, String verdict, String avoid,
String summary, String detail, String color, String gradient, String mascot) {
this.id = id;
this.title = title;
this.category = category;
this.categoryIcon = categoryIcon;
this.verdict = verdict;
this.avoid = avoid;
this.summary = summary;
this.detail = detail;
this.color = color;
this.gradient = gradient;
this.mascot = mascot;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getCategory() {
return category;
}
public String getCategoryIcon() {
return categoryIcon;
}
public String getVerdict() {
return verdict;
}
public String getAvoid() {
return avoid;
}
public String getSummary() {
return summary;
}
public String getDetail() {
return detail;
}
public String getColor() {
return color;
}
public String getGradient() {
return gradient;
}
public String getMascot() {
return mascot;
}
}
public static class LifeTip {
private String id;
private String title;
private String icon;
private String content;
private String color;
private String mark;
public LifeTip(String id, String title, String icon, String content, String color, String mark) {
this.id = id;
this.title = title;
this.icon = icon;
this.content = content;
this.color = color;
this.mark = mark;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getIcon() {
return icon;
}
public String getContent() {
return content;
}
public String getColor() {
return color;
}
public String getMark() {
return mark;
}
}
public static class PinCardResponse {
private String pinnedCardId;
public PinCardResponse(String pinnedCardId) {
this.pinnedCardId = pinnedCardId;
}
public String getPinnedCardId() {
return pinnedCardId;
}
}
}
@@ -0,0 +1,146 @@
package com.hongzhen.hera.daily;
import com.hongzhen.hera.common.BusinessException;
import com.hongzhen.hera.daily.DailyDtos.CalendarDay;
import com.hongzhen.hera.daily.DailyDtos.DailyHomeResponse;
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.nlf.calendar.Solar;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DailyService {
private final UserRepository userRepository;
public DailyService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public DailyHomeResponse home(Long userId) {
User user = user(userId);
LocalDate today = LocalDate.now();
return new DailyHomeResponse(
today,
lunarDate(today),
weekName(today.getDayOfWeek()),
"小逆",
"晚上好," + user.getNickname() + "!",
"今日宜静少思少虑,让身心在休整中蓄能。",
calendarDays(today),
luckyPanels(),
cards(),
lifeTips(),
user.getPinnedDailyCardId()
);
}
@Transactional
public String pin(Long userId, String cardId) {
boolean exists = false;
for (FortuneCard card : cards()) {
if (card.getId().equals(cardId)) {
exists = true;
break;
}
}
if (!exists) {
throw new BusinessException("卡牌不存在");
}
User user = user(userId);
String nextPinnedCardId = cardId.equals(user.getPinnedDailyCardId()) ? null : cardId;
user.setPinnedDailyCardId(nextPinnedCardId);
user.touch();
return nextPinnedCardId;
}
private User user(Long userId) {
return userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));
}
private String lunarDate(LocalDate date) {
return Solar.fromYmd(date.getYear(), date.getMonthValue(), date.getDayOfMonth()).getLunar().toString();
}
private List<CalendarDay> calendarDays(LocalDate today) {
LocalDate prev = today.minusDays(1);
LocalDate next = today.plusDays(1);
return Arrays.asList(
new CalendarDay(prev.getDayOfMonth(), weekName(prev.getDayOfWeek()), "小吉"),
new CalendarDay(today.getDayOfMonth(), weekName(today.getDayOfWeek()), "今日"),
new CalendarDay(next.getDayOfMonth(), weekName(next.getDayOfWeek()), "平日")
);
}
private String weekName(DayOfWeek dayOfWeek) {
String[] names = {"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
return names[dayOfWeek.getValue() - 1];
}
private List<LuckyPanel> luckyPanels() {
return Arrays.asList(
new LuckyPanel("吉色", "●\n宝象\n蓝牙白", "#34449a"),
new LuckyPanel("吉向", "西\n北", "#a26711"),
new LuckyPanel("吉时", "申 15点~17点\n丑 01点~03点", "#a26711"),
new LuckyPanel("吉数", "1 / 9", "#a26711")
);
}
private List<FortuneCard> cards() {
return Arrays.asList(
new FortuneCard("travel", "逐风而行\n思绪开阔", "出行", "🍡", "宜", "忌",
"今天去滨江步道,穿宽松卫衣,戴耳机听民谣,让风吹起头发,看江面波浪,烦心事都被吹走啦。",
"出行宜慢行与看水。风动而心开,走到有水、有树、有开阔视野的地方,能把卡住的念头松开。若需远行,提前十五分钟出门,少赶路,多留白。",
"#f05b45", "linear-gradient(180deg,#ff7569 0%,#fffaf0 76%)", "🏮"),
new FortuneCard("wealth", "盲目扩张\n财气耗损", "财富", "🟠", "忌", "宜",
"今天别一口气报多个付费课程或囤货,先推进手头正在做的事,把精力放在可控的小范围里更踏实。",
"财富能量不宜过度分散,需聚焦核心,避免同时开启太多新尝试。今日翼宿值日,木气稍显。身弱之人本需金水滋养,然木旺则耗金生火,易令财源如春藤蔓生,看似繁茂却暗耗根基。",
"#f17422", "linear-gradient(180deg,#ff9d49 0%,#fffaf0 76%)", "🪙"),
new FortuneCard("life", "勿过度应酬\n多独处", "生活", "🍶", "忌", "宜",
"推掉不必要的酒局,在家煮一碗热汤面,看一部喜欢的纪录片,享受独处的安静时光。",
"生活宜收束边界,少用热闹消耗自己。把房间收拾出一块干净角落,吃温热简单的食物,留给身体一点安静,今日的气会慢慢回到你身上。",
"#f5b700", "linear-gradient(180deg,#ffd91f 0%,#fffaf0 76%)", "🍶"),
new FortuneCard("study", "秋水沉静\n深度思考问题", "学业", "🎋", "宜", "忌",
"找个安静的咖啡馆,点一杯热拿铁,对着一个知识点写一篇小感悟,秋日的沉静会让思考更有深度。",
"翼宿主文昌,水势沉静时。今日水星得势,怡应身弱之需,金水相生,思绪如秋水澄澈。木火土三忌退避,杂念不生,正宜深潜学海。金为水源,得此加持,逻辑脉络自明;水主智识,沉浸其中,难题可化涟漪。",
"#20b98f", "linear-gradient(180deg,#29c89f 0%,#fffaf0 76%)", "🎍"),
new FortuneCard("career", "情绪躁动\n避免冒进", "事业", "🌬", "忌", "宜",
"今天提交方案前,先找信任的同事帮忙过一遍细节,或者把方案放半小时再看一遍,确保没有疏漏后再行动。",
"今日容易冲动行事,适合稳扎稳打,不要贸然提交不成熟的方案。火气炽盛,于身弱喜金水者而言,更需警惕言语冲动引燃事端。宜效法秋水之静,涵养金气之锐。",
"#5cc4c8", "linear-gradient(180deg,#76dcd5 0%,#fffaf0 76%)", "☁"),
new FortuneCard("health", "稳守心神\n忌急躁多动", "健康", "💧", "忌", "宜",
"穿浅绿纯棉T恤,待在通风书房,喝杯加薄荷叶的温水,每隔1小时看窗外5分钟,避免情绪急躁。",
"健康宜清润、缓行、少燥。今日心火易浮,若一直急着处理细碎事务,会让身体更累。适合补水、通风、放慢脚步,晚上尽量早点休息。",
"#42aee8", "linear-gradient(180deg,#8ad8ff 0%,#fffaf0 76%)", "🪢"),
new FortuneCard("social", "深夜畅聊\n知己深谈", "社交", "🦊", "宜", "忌",
"适合和知己深度对话,解开心里的小疙瘩。",
"今日水气氤氲,润泽身弱之局。金水相生,恰逢知己深谈之机,如清泉涤尘,可解心结。翼宿主沟通,暗合水德流动之性,宜借言语之柔波,疏泄郁结。",
"#c879e8", "linear-gradient(180deg,#e8a4ff 0%,#fffaf0 76%)", "🦊"),
new FortuneCard("love", "长势放缓\n忌过度索取", "情感", "🌸", "忌", "宜",
"今日情感节奏偏缓,别给关系施加太多期待。",
"今日翼宿值守,木气升腾。身弱之人逢木旺,如藤蔓缠枝,易生过度索求之心。金水为喜而难制木,情感如溪流遇石,节奏自当放缓。莫强求炽热,宜守静观澜。",
"#f06aa6", "linear-gradient(180deg,#ff7fbc 0%,#fffaf0 76%)", "🌸")
);
}
private List<LifeTip> lifeTips() {
return Arrays.asList(
new LifeTip("travel", "出行", "🍡", "今天去滨江步道,穿宽松卫衣,戴耳机听民谣,让风吹起头发,看江面波浪,烦心事都被吹走啦。", "#ff7569", "灯笼"),
new LifeTip("wealth", "财富", "🟠", "今天别一口气报多个付费课程或囤货,先推进手头正在做的事,把精力放在可控的小范围里更踏实。", "#ff9d49", "铜钱"),
new LifeTip("life", "生活", "🍶", "推掉不必要的酒局,在家煮一碗热汤面,看一部喜欢的纪录片,享受独处的安静时光。", "#ffe75a", "葫芦"),
new LifeTip("study", "学业", "🎋", "找个安静的咖啡馆,点一杯热拿铁,对着一个知识点写一篇小感悟,秋日的沉静会让思考更有深度。", "#96efc7", "书卷"),
new LifeTip("career", "事业", "🌬", "今天提交方案前,先找信任的同事帮忙过一遍细节,或者把方案放半小时再看一遍,确保没有疏漏后再行动。", "#70e4df", "祥云"),
new LifeTip("health", "健康", "💧", "穿浅绿纯棉T恤,待在通风书房,喝杯加薄荷叶的温水,每隔1小时看窗外5分钟,避免情绪急躁。", "#86d4ff", "绳结"),
new LifeTip("social", "社交", "🦊", "晚上和兄弟约在楼下的烧烤摊,点几串烤串,说说最近工作上的压力,不用掩饰脆弱,懂你的人自然会懂。", "#e7a3ff", "狐狸"),
new LifeTip("love", "情感", "🌸", "别追问对方为什么不秒回消息,可以自己插一瓶小雏菊在书桌,读一本短篇散文,给彼此留一点呼吸空间。", "#ff89c4", "花")
);
}
}
@@ -0,0 +1,206 @@
package com.hongzhen.hera.fortune;
import com.hongzhen.hera.user.ProfileDtos.ElementReport;
import com.hongzhen.hera.user.ProfileDtos.ElementScore;
import com.nlf.calendar.EightChar;
import com.nlf.calendar.Solar;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class FortuneService {
private static final List<String> ELEMENTS = Arrays.asList("木", "火", "土", "金", "水");
private static final Map<String, String> STEM_ELEMENTS = new HashMap<String, String>();
private static final Map<String, String> BRANCH_ELEMENTS = new HashMap<String, String>();
private static final Map<String, Integer> HOUR_STARTS = new HashMap<String, Integer>();
private static final Map<String, String> COLORS = new HashMap<String, String>();
static {
STEM_ELEMENTS.put("甲", "木");
STEM_ELEMENTS.put("乙", "木");
STEM_ELEMENTS.put("丙", "火");
STEM_ELEMENTS.put("丁", "火");
STEM_ELEMENTS.put("戊", "土");
STEM_ELEMENTS.put("己", "土");
STEM_ELEMENTS.put("庚", "金");
STEM_ELEMENTS.put("辛", "金");
STEM_ELEMENTS.put("壬", "水");
STEM_ELEMENTS.put("癸", "水");
BRANCH_ELEMENTS.put("寅", "木");
BRANCH_ELEMENTS.put("卯", "木");
BRANCH_ELEMENTS.put("巳", "火");
BRANCH_ELEMENTS.put("午", "火");
BRANCH_ELEMENTS.put("辰", "土");
BRANCH_ELEMENTS.put("戌", "土");
BRANCH_ELEMENTS.put("丑", "土");
BRANCH_ELEMENTS.put("未", "土");
BRANCH_ELEMENTS.put("申", "金");
BRANCH_ELEMENTS.put("酉", "金");
BRANCH_ELEMENTS.put("亥", "水");
BRANCH_ELEMENTS.put("子", "水");
HOUR_STARTS.put("子时", 0);
HOUR_STARTS.put("丑时", 1);
HOUR_STARTS.put("寅时", 3);
HOUR_STARTS.put("卯时", 5);
HOUR_STARTS.put("辰时", 7);
HOUR_STARTS.put("巳时", 9);
HOUR_STARTS.put("午时", 11);
HOUR_STARTS.put("未时", 13);
HOUR_STARTS.put("申时", 15);
HOUR_STARTS.put("酉时", 17);
HOUR_STARTS.put("戌时", 19);
HOUR_STARTS.put("亥时", 21);
COLORS.put("木", "#53d83c");
COLORS.put("火", "#ff4b88");
COLORS.put("土", "#b98224");
COLORS.put("金", "#ffd316");
COLORS.put("水", "#13cce9");
}
public ElementReport calculate(LocalDate birthday, String birthHour) {
int hour = HOUR_STARTS.containsKey(birthHour) ? HOUR_STARTS.get(birthHour) : 0;
EightChar eightChar = Solar.fromYmdHms(
birthday.getYear(),
birthday.getMonthValue(),
birthday.getDayOfMonth(),
hour,
0,
0
).getLunar().getEightChar();
Map<String, Integer> weights = initialWeights();
addPillar(weights, eightChar.getYear(), 10, 10);
addPillar(weights, eightChar.getMonth(), 10, 18);
addPillar(weights, eightChar.getDay(), 12, 10);
addPillar(weights, eightChar.getTime(), 10, 10);
List<ElementScore> scores = toScores(weights);
String dayMaster = eightChar.getDayGan();
String dayMasterElement = elementOfStem(dayMaster);
String favorable = favorableElements(dayMasterElement, weights);
String unfavorable = unfavorableElements(favorable);
String year = eightChar.getYear();
String month = eightChar.getMonth();
String day = eightChar.getDay();
String time = eightChar.getTime();
return new ElementReport(scores, favorable, unfavorable, dayMaster, year + " " + month + " " + day + " " + time,
year, month, day, time);
}
private Map<String, Integer> initialWeights() {
Map<String, Integer> weights = new HashMap<String, Integer>();
for (String element : ELEMENTS) {
weights.put(element, 0);
}
return weights;
}
private void addPillar(Map<String, Integer> weights, String pillar, int stemWeight, int branchWeight) {
if (pillar == null || pillar.length() < 2) {
return;
}
addWeight(weights, STEM_ELEMENTS.get(pillar.substring(0, 1)), stemWeight);
addWeight(weights, BRANCH_ELEMENTS.get(pillar.substring(1, 2)), branchWeight);
}
private void addWeight(Map<String, Integer> weights, String element, int weight) {
if (element != null) {
weights.put(element, weights.get(element) + weight);
}
}
private List<ElementScore> toScores(Map<String, Integer> weights) {
int total = 0;
for (Integer weight : weights.values()) {
total += weight;
}
List<ElementScore> scores = new ArrayList<ElementScore>();
for (String element : ELEMENTS) {
int value = total == 0 ? 0 : Math.round(weights.get(element) * 100f / total);
scores.add(new ElementScore(element, COLORS.get(element), value));
}
return scores;
}
private String favorableElements(String dayMaster, Map<String, Integer> weights) {
String supporter = produces(dayMaster);
int selfAndSupport = weights.get(dayMaster) + weights.get(supporter);
if (selfAndSupport < 45) {
return supporter + dayMaster;
}
return producedBy(dayMaster) + controlledBy(dayMaster);
}
private String unfavorableElements(String favorable) {
StringBuilder builder = new StringBuilder();
for (String element : ELEMENTS) {
if (!favorable.contains(element)) {
builder.append(element);
}
}
return builder.toString();
}
private String elementOfStem(String stem) {
String element = STEM_ELEMENTS.get(stem);
return element == null ? "土" : element;
}
private String produces(String element) {
if ("木".equals(element)) {
return "水";
}
if ("火".equals(element)) {
return "木";
}
if ("土".equals(element)) {
return "火";
}
if ("金".equals(element)) {
return "土";
}
return "金";
}
private String producedBy(String element) {
if ("木".equals(element)) {
return "火";
}
if ("火".equals(element)) {
return "土";
}
if ("土".equals(element)) {
return "金";
}
if ("金".equals(element)) {
return "水";
}
return "木";
}
private String controlledBy(String element) {
if ("木".equals(element)) {
return "土";
}
if ("火".equals(element)) {
return "金";
}
if ("土".equals(element)) {
return "水";
}
if ("金".equals(element)) {
return "木";
}
return "火";
}
}
@@ -0,0 +1,31 @@
package com.hongzhen.hera.points;
import com.hongzhen.hera.common.ApiResponse;
import com.hongzhen.hera.security.CurrentUser;
import com.hongzhen.hera.user.UserService;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/points")
public class PointsController {
private final UserService userService;
public PointsController(UserService userService) {
this.userService = userService;
}
@GetMapping("/exchange-status")
public ApiResponse<Map<String, Object>> exchangeStatus() {
int points = userService.get(CurrentUser.id()).getPoints();
Map<String, Object> data = new HashMap<String, Object>();
data.put("points", points);
data.put("available", false);
data.put("message", "你当前拥有 " + points + " 吉分,兑换功能即将开放,敬请期待!");
return ApiResponse.ok(data);
}
}
@@ -0,0 +1,32 @@
package com.hongzhen.hera.security;
import com.hongzhen.hera.common.BusinessException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final JwtService jwtService;
public AuthInterceptor(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String auth = request.getHeader("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
throw new BusinessException("请先登录");
}
CurrentUser.set(jwtService.parseUserId(auth.substring(7)));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
CurrentUser.clear();
}
}
@@ -0,0 +1,21 @@
package com.hongzhen.hera.security;
public final class CurrentUser {
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
private CurrentUser() {
}
public static void set(Long userId) {
USER_ID.set(userId);
}
public static Long id() {
return USER_ID.get();
}
public static void clear() {
USER_ID.remove();
}
}
@@ -0,0 +1,33 @@
package com.hongzhen.hera.security;
import com.hongzhen.hera.config.HeraProperties;
import io.jsonwebtoken.Jwts;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Service;
@Service
public class JwtService {
private final byte[] key;
public JwtService(HeraProperties properties) {
this.key = properties.getJwtSecret().getBytes(StandardCharsets.UTF_8);
}
public String issue(Long userId) {
Instant now = Instant.now();
return Jwts.builder()
.setSubject(String.valueOf(userId))
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plusSeconds(60L * 60L * 24L * 30L)))
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}
public Long parseUserId(String token) {
return Long.valueOf(Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getSubject());
}
}
@@ -0,0 +1,218 @@
package com.hongzhen.hera.user;
import java.time.LocalDate;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class ProfileDtos {
public static class ProfileResponse {
private Long id;
private String nickname;
private String phone;
private String gender;
private LocalDate birthday;
private String birthHour;
private String province;
private String city;
private String district;
private int points;
private int remainingEdits;
private ElementReport elementReport;
public ProfileResponse(Long id, String nickname, String phone, 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.gender = gender;
this.birthday = birthday;
this.birthHour = birthHour;
this.province = province;
this.city = city;
this.district = district;
this.points = points;
this.remainingEdits = remainingEdits;
this.elementReport = elementReport;
}
public Long getId() {
return id;
}
public String getNickname() {
return nickname;
}
public String getPhone() {
return phone;
}
public String getGender() {
return gender;
}
public LocalDate getBirthday() {
return birthday;
}
public String getBirthHour() {
return birthHour;
}
public String getProvince() {
return province;
}
public String getCity() {
return city;
}
public String getDistrict() {
return district;
}
public int getPoints() {
return points;
}
public int getRemainingEdits() {
return remainingEdits;
}
public ElementReport getElementReport() {
return elementReport;
}
}
public static class ElementReport {
private List<ElementScore> scores;
private String favorable;
private String unfavorable;
private String dayMaster;
private String bazi;
private String yearPillar;
private String monthPillar;
private String dayPillar;
private String timePillar;
public ElementReport(List<ElementScore> scores, String favorable, String unfavorable, String dayMaster,
String bazi, String yearPillar, String monthPillar, String dayPillar, String timePillar) {
this.scores = scores;
this.favorable = favorable;
this.unfavorable = unfavorable;
this.dayMaster = dayMaster;
this.bazi = bazi;
this.yearPillar = yearPillar;
this.monthPillar = monthPillar;
this.dayPillar = dayPillar;
this.timePillar = timePillar;
}
public List<ElementScore> getScores() {
return scores;
}
public String getFavorable() {
return favorable;
}
public String getUnfavorable() {
return unfavorable;
}
public String getDayMaster() {
return dayMaster;
}
public String getBazi() {
return bazi;
}
public String getYearPillar() {
return yearPillar;
}
public String getMonthPillar() {
return monthPillar;
}
public String getDayPillar() {
return dayPillar;
}
public String getTimePillar() {
return timePillar;
}
}
public static class ElementScore {
private String name;
private String color;
private int value;
public ElementScore(String name, String color, int value) {
this.name = name;
this.color = color;
this.value = value;
}
public String getName() {
return name;
}
public String getColor() {
return color;
}
public int getValue() {
return value;
}
}
public static class UpdateProfileRequest {
@NotBlank
private String nickname;
@NotBlank
private String gender;
@NotNull
private LocalDate birthday;
@NotBlank
private String birthHour;
@NotBlank
private String province;
@NotBlank
private String city;
@NotBlank
private String district;
public String getNickname() {
return nickname;
}
public String getGender() {
return gender;
}
public LocalDate getBirthday() {
return birthday;
}
public String getBirthHour() {
return birthHour;
}
public String getProvince() {
return province;
}
public String getCity() {
return city;
}
public String getDistrict() {
return district;
}
}
}
@@ -0,0 +1,157 @@
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)
private String openId;
private String unionId;
private String phone;
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;
private String pinnedDailyCardId;
private int profileEditCount = 0;
private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt = LocalDateTime.now();
public Long getId() {
return id;
}
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getUnionId() {
return unionId;
}
public void setUnionId(String unionId) {
this.unionId = unionId;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public LocalDate getBirthday() {
return birthday;
}
public void setBirthday(LocalDate birthday) {
this.birthday = birthday;
}
public String getBirthHour() {
return birthHour;
}
public void setBirthHour(String birthHour) {
this.birthHour = birthHour;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getDistrict() {
return district;
}
public void setDistrict(String district) {
this.district = district;
}
public int getPoints() {
return points;
}
public void setPoints(int points) {
this.points = points;
}
public String getPinnedDailyCardId() {
return pinnedDailyCardId;
}
public void setPinnedDailyCardId(String pinnedDailyCardId) {
this.pinnedDailyCardId = pinnedDailyCardId;
}
public int getProfileEditCount() {
return profileEditCount;
}
public void setProfileEditCount(int profileEditCount) {
this.profileEditCount = profileEditCount;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void touch() {
updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,33 @@
package com.hongzhen.hera.user;
import com.hongzhen.hera.common.ApiResponse;
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.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;
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/profile")
public ApiResponse<ProfileResponse> profile() {
return ApiResponse.ok(userService.profile(CurrentUser.id()));
}
@PutMapping("/profile")
public ApiResponse<ProfileResponse> update(@Valid @RequestBody UpdateProfileRequest request) {
return ApiResponse.ok(userService.updateProfile(CurrentUser.id(), request));
}
}
@@ -0,0 +1,9 @@
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);
}
@@ -0,0 +1,65 @@
package com.hongzhen.hera.user;
import com.hongzhen.hera.common.BusinessException;
import com.hongzhen.hera.fortune.FortuneService;
import com.hongzhen.hera.user.ProfileDtos.ElementReport;
import com.hongzhen.hera.user.ProfileDtos.ProfileResponse;
import com.hongzhen.hera.user.ProfileDtos.UpdateProfileRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
private final FortuneService fortuneService;
public UserService(UserRepository userRepository, FortuneService fortuneService) {
this.userRepository = userRepository;
this.fortuneService = fortuneService;
}
public User get(Long userId) {
return userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));
}
public ProfileResponse profile(Long userId) {
return toProfile(get(userId));
}
@Transactional
public ProfileResponse updateProfile(Long userId, UpdateProfileRequest request) {
User user = get(userId);
if (user.getProfileEditCount() >= 5) {
throw new BusinessException("半年内只能修改五次哦");
}
user.setNickname(request.getNickname());
user.setGender(request.getGender());
user.setBirthday(request.getBirthday());
user.setBirthHour(request.getBirthHour());
user.setProvince(request.getProvince());
user.setCity(request.getCity());
user.setDistrict(request.getDistrict());
user.setProfileEditCount(user.getProfileEditCount() + 1);
user.touch();
return toProfile(user);
}
public ProfileResponse toProfile(User user) {
ElementReport report = fortuneService.calculate(user.getBirthday(), user.getBirthHour());
return new ProfileResponse(
user.getId(),
user.getNickname(),
user.getPhone(),
user.getGender(),
user.getBirthday(),
user.getBirthHour(),
user.getProvince(),
user.getCity(),
user.getDistrict(),
user.getPoints(),
Math.max(0, 5 - user.getProfileEditCount()),
report
);
}
}
+29
View File
@@ -0,0 +1,29 @@
server:
port: 8080
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
hera:
jwt-secret: "replace-this-development-secret-replace-this-development-secret"
wx:
app-id: "${WECHAT_APP_ID:wxa76a2ef9e8ba612e}"
app-secret: "${WECHAT_APP_SECRET:}"
mock: false
sms:
mock: true