feat: add hera backend
This commit is contained in:
1 parent
5b906bf8f3
commit
4222b7124c
34 files changed
+2023
-83
No files matched your search
@@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
target/
|
||||
*.iml
|
||||
|
||||
@@ -1,92 +1,24 @@
|
||||
# hera
|
||||
# Hera 后端
|
||||
|
||||
Spring Boot 后端,提供微信小程序登录、手机号验证码绑定、用户资料、协议、关注我们和吉分兑换状态接口。
|
||||
|
||||
命理计算使用 `cn.6tail:lunar` 根据公历生日和时辰生成四柱八字,再计算五行分布、日主、喜用和忌用。
|
||||
|
||||
## Getting started
|
||||
元宝页提供今日卡牌、每日生活小贴士、卡牌置顶等接口。
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
## 本地启动
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
|
||||
## Add your files
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin http://101.43.139.56/git/hz/hera.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```bash
|
||||
export WECHAT_APP_ID=wxa76a2ef9e8ba612e
|
||||
export WECHAT_APP_SECRET=266bb34a7186a4b835186141516efb78
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
默认地址:`http://localhost:8080`
|
||||
|
||||
- [ ] [Set up project integrations](http://101.43.139.56/git/hz/hera/-/settings/integrations)
|
||||
## 开发说明
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
|
||||
## Test and Deploy
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
- 当前已默认关闭微信 mock,并接入微信 `jscode2session`。
|
||||
- `WECHAT_APP_SECRET` 请优先使用服务器环境变量配置,不建议提交到公开仓库。
|
||||
- `/api/auth/sms` 开发阶段返回 `mockCode`,接入短信厂商后移除返回值。
|
||||
- 正式上线前需要替换 `hera.jwt-secret`、短信服务实现和协议正文。
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.18</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.hongzhen</groupId>
|
||||
<artifactId>hera</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>hera</name>
|
||||
<description>Hera WeChat mini program backend</description>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.6tail</groupId>
|
||||
<artifactId>lunar</artifactId>
|
||||
<version>1.7.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user