From f2c153e3acea21f34e2a04f7f3ac446708b157b4 Mon Sep 17 00:00:00 2001
From: H1d3rOne <260588178@qq.com>
Date: Thu, 5 Mar 2026 23:03:21 +0800
Subject: [PATCH 1/8] =?UTF-8?q?Initial=20commit:=20=E6=B7=BB=E5=8A=A0READM?=
=?UTF-8?q?E.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 273 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 273 insertions(+)
create mode 100644 README.md
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3745a67
--- /dev/null
+++ b/README.md
@@ -0,0 +1,273 @@
+
+ 排盘工具 - 升级版
+
+
+
+
+
+
+
+
+
+
+ 在线地址:
+
+ https://www.muxuange.cn/paipan
+
+
+
+ 版本:v2.2.0 | 运行环境:jdk8
+
+
+```
+公历2025年8月22日更新
+
+此仓库唯一,开源免费、不售卖、仅供参考使用
+https://gitee.com/shan-dai/xuan-utils-pro
+```
+
+## **八字排盘**
+
+🔈八字排盘提供基础数据,如:八字、主星、地势、空亡、神煞等。
+
+### 代码示例
+
+```java
+public class BaZiTest {
+ public static void main(String[] args) {
+
+ // 1、基础设置(可选)
+ BaZiJiChuSetting baZiJiChuSetting = new BaZiJiChuSetting();
+ baZiJiChuSetting.setSex(1); // 性别(0:女。1:男)
+ // 更多基础设置...
+
+ // 2、神煞设置(可选)
+ BaZiShenShaSetting baZiShenShaSetting = new BaZiShenShaSetting();
+ baZiShenShaSetting.setTaiJiGuiRen(0); // 太极贵人(0:显示。1:关闭)
+ // 更多神煞设置...
+
+ // 3、干支留意设置(可选)
+ BaZiGanZhiLiuYiSetting baZiGanZhiLiuYiSetting = new BaZiGanZhiLiuYiSetting();
+ baZiGanZhiLiuYiSetting.setTianGanXiangSheng(0); // 天干相生(0:显示。1:关闭)
+ // 更多干支留意设置...
+
+ // 4、初始化
+ BaZi baZi = new BaZi(baZiJiChuSetting); // 使用基础设置初始化
+ baZi.baZiShenShaSetting(baZiShenShaSetting); // 神煞设置
+ baZi.baZiGanZhiLiuYiSetting(baZiGanZhiLiuYiSetting); // 干支留意设置
+
+ // 5、输出结果
+ System.out.println(baZi);
+
+ }
+}
+```
+
+输出结果(部分数据):
+
+```
+公历:2024年01月01日00时00分00秒 农历:二〇二三年冬月二十(早)子时 星期:周一 季节:仲冬 生肖:兔 星座:摩羯座 月相:更待 月将:丑 月将神:大吉 五不遇时:false 年干支主星:正印 月干支主星:比肩 日干支主星:元男 时干支主星:比肩 年支藏干:[乙] 月支藏干:[癸] 日支藏干:[癸] 时支藏干:[癸] 年干支副星:[劫财] 月干支副星:[正印] 日干支副星:[正印] 时干支副星:[正印] 年干支自坐:长生 月干支自坐:沐浴 日干支自坐:沐浴 时干支自坐:沐浴 年干支星运:帝旺 月干支星运:沐浴 日干支星运:沐浴 时干支星运:沐浴年干支神煞:[天乙贵人, 福星贵人, 文昌贵人, 天厨贵人, 德秀贵人, 羊刃] 月干支神煞:[太极贵人, 福星贵人, 德秀贵人, 将星, 桃花, 红鸾, 披麻] 日干支神煞:[太极贵人, 福星贵人, 德秀贵人, 阴注阳受, 冲天煞, 桃花, 红鸾, 天赦, 披麻] 时干支神煞:[太极贵人, 福星贵人, 德秀贵人, 阴注阳受, 冲天煞, 将星, 桃花, 红鸾, 披麻]
+```
+
+### 效果预览
+
+
+
+=========================================================
+
+## **六爻排盘**
+
+🔈六爻排盘提供基础数据,如:八字、五行、空亡、世应、六亲等。
+
+### 代码示例
+
+```java
+public class LiuYaoTest {
+ public static void main(String[] args) {
+
+ // 1、基础设置(可选)
+ LiuYaoJiChuSetting liuYaoJiChuSetting = new LiuYaoJiChuSetting();
+ baZiJiChuSetting.setSex(1); // 性别(0:女。1:男)
+ // 更多基础设置...
+
+ // 2、神煞设置(可选)
+ LiuYaoShenShaSetting liuYaoShenShaSetting = new LiuYaoShenShaSetting();
+ liuYaoShenShaSetting.setTaiJiGuiRen(0); // 太极贵人(0:显示。1:关闭)
+ // 更多神煞设置...
+
+ // 3、初始化
+ LiuYao liuYao = new LiuYao(liuYaoJiChuSetting); // 使用基础设置初始化
+ liuYao.liuYaoShenShaSetting(liuYaoShenShaSetting); // 神煞设置
+
+ // 4、输出结果
+ System.out.println(liuYao);
+
+ }
+}
+```
+
+输出结果(部分数据):
+
+```
+公历:2024年01月01日00时00分00秒 农历:二〇二三年冬月二十(早)子时 星期:周一 季节:仲冬 生肖:兔 星座:摩羯座 月相:更待 月将:丑 月将神:大吉 五不遇时:false 上卦:离(☲) 下卦:震(☳) 本卦:火雷噬嗑(䷔) 变卦:震为雷(䷲) 互卦:水山蹇(䷦) 错卦:水风井(䷯) 综卦:山火贲(䷕) 伏卦:巽为风(䷸)
+```
+
+### 效果预览
+
+
+
+=========================================================
+
+## **大六壬排盘**
+
+🔈大六壬排盘提供基础数据,如:地盘、天盘、人盘、四课、三传等。
+
+### 代码示例
+
+```java
+public class DaLiuRenTest {
+ public static void main(String[] args) {
+
+ // 1、基础设置(可选)
+ DaLiuRenJiChuSetting daLiuRenJiChuSetting = new DaLiuRenJiChuSetting();
+ daLiuRenJiChuSetting.setSex(1); // 性别(0:女。1:男)
+ // 更多基础设置...
+
+ // 2、初始化
+ DaLiuRen daLiuRen = new DaLiuRen(daLiuRenJiChuSetting); // 使用基础设置初始化
+
+ // 3、输出结果
+ System.out.println(daLiuRen);
+
+ }
+}
+```
+
+输出结果(部分数据):
+
+```
+公历:2024年01月01日00时00分00秒 农历:二〇二三年冬月二十(早)子时 星期:周一 季节:仲冬 生肖:兔 星座:摩羯座 月相:更待 月将:丑 月将神:大吉 五不遇时:false 天地盘类型:进连茹盘 地盘:[寅, 卯, 辰, 巳, 午, 未, 申, 酉, 戌, 亥, 子, 丑] 天盘:[卯, 辰, 巳, 午, 未, 申, 酉, 戌, 亥, 子, 丑, 寅] 神盘:[勾陈, 六合, 朱雀, 螣蛇, 贵人, 天后, 太阴, 玄武, 太常, 白虎, 天空, 青龙] 四课:[[卯, 甲], [辰, 卯], [丑, 子], [寅, 丑]] 三传:[辰, 巳, 午]
+```
+
+### 效果预览
+
+
+
+=========================================================
+
+## **奇门遁甲排盘**
+
+🔈奇门遁甲排盘提供基础数据,如:地盘、天盘、人盘、神盘、动应、克应等。
+
+### 代码示例(转盘)
+
+```java
+public class QiMenTest {
+ public static void main(String[] args) {
+
+ // 1、基础设置(可选)
+ QiMenZhuanPanJiChuSetting qiMenZhuanPanJiChuSetting = new QiMenZhuanPanJiChuSetting();
+ qiMenZhuanPanJiChuSetting.setSex(1); // 性别(0:女。1:男)
+ // 更多基础设置...
+
+ // 2、初始化
+ QiMenZhuanPan qiMen = new QiMenZhuanPan(qiMenZhuanPanJiChuSetting); // 使用基础设置初始化
+
+ // 3、输出结果
+ System.out.println(qiMen);
+
+ }
+}
+```
+
+输出结果(部分数据):
+
+```
+公历:2024年01月01日00时00分00秒 农历:二〇二三年冬月二十(早)子时 星期:周一 季节:仲冬 生肖:兔 星座:摩羯座 月相:更待 月将:丑 月将神:大吉 五不遇时:false 符头:甲子 节气:冬至上元 局数:阳遁1局 旬首:甲子 值符:天蓬 值使:休门 天乙:天蓬 地乙:休门 太乙:休门 地盘:[戊, 己, 庚, 辛, 壬, 癸, 丁, 丙, 乙] 天盘:[天蓬, 芮禽, 天冲, 天辅, , 天心, 天柱, 天任, 天英] 人盘:[休门, 死门, 伤门, 杜门, , 开门, 惊门, 生门, 景门] 神盘:[值符, 玄武, 太阴, 六合, , 九天, 九地, 螣蛇, 白虎] 六仪击刑:[己击刑(坤二宫)] 奇仪入墓:[辛入墓(巽四宫)]
+```
+
+### 效果预览(转盘)
+
+
+
+=========================================================
+
+## **梅花易数排盘**
+
+🔈梅花易数排盘提供基础数据,如:上卦、下卦、本卦、变卦、互卦等。
+
+### 代码示例
+
+```java
+public class MeiHuaTest {
+ public static void main(String[] args) {
+
+ // 1、基础设置(可选)
+ MeiHuaJiChuSetting meiHuaJiChuSetting = new MeiHuaJiChuSetting();
+ meiHuaJiChuSetting.setSex(1); // 性别(0:女。1:男)
+ // 更多基础设置...
+
+ // 2、初始化
+ MeiHua meiHua = new MeiHua(meiHuaJiChuSetting); // 使用基础设置初始化
+
+ // 3、输出结果
+ System.out.println(meiHua);
+
+ }
+}
+```
+
+输出结果(部分数据):
+
+```
+公历:2024年01月01日00时00分00秒 农历:二〇二三年冬月二十(早)子时 星期:周一 季节:仲冬 生肖:兔 星座:摩羯座 月相:更待 月将:丑 月将神:大吉 五不遇时:false 卦码:346 上卦:离(☲) 下卦:震(☳) 动爻:6 本卦:火雷噬嗑(䷔) 变卦:震为雷(䷲) 互卦:水山蹇(䷦) 错卦:水风井(䷯) 综卦:山火贲(䷕)
+```
+
+### 效果预览
+
+
+
+=========================================================
+
+## **紫微斗数排盘**
+
+🔈紫微斗数排盘提供基础数据,如:命宫、身宫、四化、诸星等。
+
+### 代码示例
+
+```java
+public class ZiWeiTest {
+ public static void main(String[] args) {
+
+ // 1、基础设置(可选)
+ ZiWeiJiChuSetting ziWeiJiChuSetting = new ZiWeiJiChuSetting();
+ ziWeiJiChuSetting.setSex(1); // 性别(0:女。1:男)
+ // 更多基础设置...
+
+ // 2、初始化
+ ZiWei ziWei = new ZiWei(ziWeiJiChuSetting); // 使用基础设置初始化
+
+ // 3、输出结果
+ System.out.println(ziWei);
+
+ }
+}
+```
+
+输出结果(部分数据):
+
+```
+公历:2024年01月01日00时00分00秒 农历:二〇二三年冬月二十(早)子时 星期:周一 季节:仲冬 生肖:兔 星座:摩羯座 月相:更待 月将:丑 月将神:大吉 五不遇时:false 五行局:金四局 化禄星宫位解读:化禄星落财帛宫,善于理财,财源滚滚。 化权星宫位解读:化权星落父母宫,父母强势,有矛盾。 化科星宫位解读:化科星落兄弟宫,兄弟有名声,助力多。 化忌星宫位解读:化忌星落命宫宫,不听劝,易焦虑,年少顺利。
+```
+
+### 效果预览
+
+
+
+=========================================================
+
+## **更多功能正在完善中...**
+
+
+ 温馨提示:代码仅供参考,不得违法违规使用,否则后果自负!
+
\ No newline at end of file
From a769cd67c4fc5d30eba5766ecc6ff654988cd49d Mon Sep 17 00:00:00 2001
From: H1d3rOne <260588178@qq.com>
Date: Thu, 5 Mar 2026 23:03:34 +0800
Subject: [PATCH 2/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0.gitignore=E6=96=87?=
=?UTF-8?q?=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 .gitignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a73858a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+target/
+.idea/
+/idea/
+.DS_Store
+*.class
+*.ipr
+*.iml
+*.iws
+*.log
+*.cache
+*.diff
+*.patch
+*.tmp
\ No newline at end of file
From 885732add965ba6046354dbc381453e2bfc8b6f7 Mon Sep 17 00:00:00 2001
From: H1d3rOne <260588178@qq.com>
Date: Thu, 5 Mar 2026 23:03:38 +0800
Subject: [PATCH 3/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0LICENSE=E6=96=87=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
LICENSE | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 LICENSE
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1d422d9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 善待
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
From c0d64dcc85bd9f2a0f9fda28c3a133b2557fd0c1 Mon Sep 17 00:00:00 2001
From: H1d3rOne <260588178@qq.com>
Date: Thu, 5 Mar 2026 23:04:05 +0800
Subject: [PATCH 4/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0pom.xml=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=E6=96=87=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 109 insertions(+)
create mode 100644 pom.xml
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..e4a719d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,109 @@
+
+
+ 4.0.0
+
+ xuan
+ xuan-utils-pro
+ 2.0-SNAPSHOT
+
+
+
+
+ 1.8
+ 2.4.5
+ 1.8
+ 1.8
+
+ UTF-8
+ UTF-8
+
+ 5.3.29
+ 9.0.78
+ 4.13.2
+ 1.2.58
+ 2.6
+ 2.6
+ 1.6
+ 3.8.1
+ 1.7.2
+ 1.7.4
+ 2.17.1
+
+
+
+
+
+
+ org.springframework
+ spring-core
+ ${spring.core.version}
+
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+ ${tomcat.embed.core.version}
+
+
+
+ junit
+ junit
+ ${junit.version}
+
+
+
+ org.projectlombok
+ lombok
+ RELEASE
+ compile
+
+
+
+ com.alibaba
+ fastjson
+ ${alibaba.fastjson.version}
+
+
+
+ commons-io
+ commons-io
+ ${commons.io.version}
+
+
+ commons-lang
+ commons-lang
+ ${commons.lang.version}
+
+
+ commons-validator
+ commons-validator
+ ${commons.validator.version}
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons.lang3.version}
+
+
+
+ org.lionsoul
+ ip2region
+ ${ip2region.versoin}
+
+
+
+ cn.6tail
+ lunar
+ ${lunar.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.databind.version}
+
+
+
+
\ No newline at end of file
From e4e3f0fdb38b050511b32db4155528d96e840b67 Mon Sep 17 00:00:00 2001
From: H1d3rOne
Date: Tue, 24 Mar 2026 11:55:29 +0800
Subject: [PATCH 5/8] feat: add h5 account auth flow
---
.../java/xuan/controller/UserController.java | 286 +++++++++++++++++-
src/main/java/xuan/entity/UserInfo.java | 12 +
.../xuan/repository/UserInfoRepository.java | 1 +
.../java/xuan/service/UserInfoService.java | 63 ++++
src/main/resources/application.properties | 11 +-
xuan-deduct-uniapp/index.html | 12 +
.../src/components/auth-modal/auth-modal.vue | 235 +++++++++++++-
xuan-deduct-uniapp/src/index.html | 17 --
xuan-deduct-uniapp/src/pages.json | 8 +
.../src/pages/auth/wechat-callback.vue | 131 ++++++++
xuan-deduct-uniapp/src/utils/api.js | 18 ++
xuan-deduct-uniapp/src/utils/authCheck.js | 25 ++
xuan-deduct-uniapp/vite.config.ts | 6 +-
13 files changed, 774 insertions(+), 51 deletions(-)
create mode 100644 xuan-deduct-uniapp/index.html
delete mode 100644 xuan-deduct-uniapp/src/index.html
create mode 100644 xuan-deduct-uniapp/src/pages/auth/wechat-callback.vue
diff --git a/src/main/java/xuan/controller/UserController.java b/src/main/java/xuan/controller/UserController.java
index d5ed3ea..fac7e01 100644
--- a/src/main/java/xuan/controller/UserController.java
+++ b/src/main/java/xuan/controller/UserController.java
@@ -2,20 +2,29 @@ package xuan.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.multipart.MultipartFile;
import xuan.entity.UserInfo;
import xuan.service.UserInfoService;
import java.io.File;
import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
+import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/api/user")
@@ -30,6 +39,15 @@ public class UserController {
@Value("${wechat.secret:}")
private String appSecret;
+
+ @Value("${wechat.web.appid:}")
+ private String webAppId;
+
+ @Value("${wechat.web.secret:}")
+ private String webAppSecret;
+
+ @Value("${wechat.web.callback-base-url:}")
+ private String webCallbackBaseUrl;
@PostMapping("/login")
public Map login(@RequestBody Map params) {
@@ -90,23 +108,10 @@ public class UserController {
}
UserInfo user = userInfoService.loginOrRegister(openId, username, avatarUrl);
-
System.out.println("登录成功: userId=" + user.getUserId() + ", openId=" + user.getOpenId());
-
- Map data = new HashMap<>();
- data.put("id", user.getId());
- data.put("userId", user.getUserId());
- data.put("openId", user.getOpenId());
- data.put("avatarUrl", user.getAvatarUrl());
- data.put("username", user.getUsername());
- data.put("phone", user.getPhone());
- data.put("vipLevel", user.getVipLevel());
- data.put("coins", user.getCoins());
- data.put("streakDays", user.getStreakDays());
- data.put("createTime", user.getCreateTime());
-
+
result.put("success", true);
- result.put("data", data);
+ result.put("data", buildUserData(user));
} catch (Exception e) {
System.err.println("登录异常: " + e.getMessage());
e.printStackTrace();
@@ -115,6 +120,117 @@ public class UserController {
}
return result;
}
+
+ @GetMapping("/wechat/h5/start")
+ public ResponseEntity startWechatH5Login(
+ @RequestParam(required = false) String returnUrl,
+ HttpServletRequest request
+ ) {
+ String targetReturnUrl = StringUtils.hasText(returnUrl)
+ ? returnUrl
+ : resolveDefaultFrontendReturnUrl(request);
+
+ if (!StringUtils.hasText(webAppId) || !StringUtils.hasText(webAppSecret)) {
+ return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未配置微信网站应用登录"));
+ }
+
+ String callbackBaseUrl = StringUtils.hasText(webCallbackBaseUrl)
+ ? webCallbackBaseUrl
+ : resolveRequestBaseUrl(request);
+
+ String callbackUrl = UriComponentsBuilder
+ .fromHttpUrl(callbackBaseUrl + "/api/user/wechat/h5/callback")
+ .queryParam("returnUrl", targetReturnUrl)
+ .build()
+ .encode()
+ .toUriString();
+
+ String wechatLoginUrl = UriComponentsBuilder
+ .fromHttpUrl("https://open.weixin.qq.com/connect/qrconnect")
+ .queryParam("appid", webAppId)
+ .queryParam("redirect_uri", callbackUrl)
+ .queryParam("response_type", "code")
+ .queryParam("scope", "snsapi_login")
+ .queryParam("state", "xuan_h5_login")
+ .build()
+ .encode()
+ .toUriString() + "#wechat_redirect";
+
+ return redirect(wechatLoginUrl);
+ }
+
+ @GetMapping("/wechat/h5/callback")
+ public ResponseEntity handleWechatH5Callback(
+ @RequestParam(required = false) String code,
+ @RequestParam(required = false) String returnUrl
+ ) {
+ String targetReturnUrl = StringUtils.hasText(returnUrl)
+ ? returnUrl
+ : "/";
+
+ if (!StringUtils.hasText(webAppId) || !StringUtils.hasText(webAppSecret)) {
+ return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未配置微信网站应用登录"));
+ }
+
+ if (!StringUtils.hasText(code)) {
+ return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未获取到微信登录凭证"));
+ }
+
+ try {
+ RestTemplate restTemplate = new RestTemplate();
+ String accessTokenUrl = UriComponentsBuilder
+ .fromHttpUrl("https://api.weixin.qq.com/sns/oauth2/access_token")
+ .queryParam("appid", webAppId)
+ .queryParam("secret", webAppSecret)
+ .queryParam("code", code)
+ .queryParam("grant_type", "authorization_code")
+ .build()
+ .encode()
+ .toUriString();
+
+ @SuppressWarnings("unchecked")
+ Map tokenResult = restTemplate.getForObject(accessTokenUrl, Map.class);
+ if (tokenResult == null || !tokenResult.containsKey("openid") || !tokenResult.containsKey("access_token")) {
+ return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, extractWechatError(tokenResult, "微信扫码登录失败")));
+ }
+
+ String openId = String.valueOf(tokenResult.get("openid"));
+ String accessToken = String.valueOf(tokenResult.get("access_token"));
+
+ String username = "微信用户";
+ String avatarUrl = "";
+ String userInfoUrl = UriComponentsBuilder
+ .fromHttpUrl("https://api.weixin.qq.com/sns/userinfo")
+ .queryParam("access_token", accessToken)
+ .queryParam("openid", openId)
+ .queryParam("lang", "zh_CN")
+ .build()
+ .encode()
+ .toUriString();
+
+ try {
+ @SuppressWarnings("unchecked")
+ Map userInfoResult = restTemplate.getForObject(userInfoUrl, Map.class);
+ if (userInfoResult != null && !userInfoResult.containsKey("errcode")) {
+ if (userInfoResult.get("nickname") != null) {
+ username = String.valueOf(userInfoResult.get("nickname"));
+ }
+ if (userInfoResult.get("headimgurl") != null) {
+ avatarUrl = String.valueOf(userInfoResult.get("headimgurl"));
+ }
+ }
+ } catch (Exception e) {
+ System.err.println("获取微信用户信息失败: " + e.getMessage());
+ }
+
+ UserInfo user = userInfoService.loginOrRegister(openId, username, avatarUrl);
+ return redirect(buildFrontendRedirectUrl(targetReturnUrl, true, null, buildUserData(user)));
+ } catch (Exception e) {
+ System.err.println("H5微信扫码回调异常: " + e.getMessage());
+ e.printStackTrace();
+ return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "微信扫码登录失败,请重试"));
+ }
+ }
@GetMapping("/info")
public Map getUserInfo(@RequestParam String userId) {
@@ -146,6 +262,41 @@ public class UserController {
}
return result;
}
+
+ @PostMapping("/h5/register")
+ public Map registerH5(@RequestBody Map params) {
+ Map result = new HashMap<>();
+ try {
+ String loginName = params.get("loginName") != null ? params.get("loginName").toString() : "";
+ String nickname = params.get("nickname") != null ? params.get("nickname").toString() : "";
+ String password = params.get("password") != null ? params.get("password").toString() : "";
+
+ UserInfo user = userInfoService.registerWithPassword(loginName, nickname, password);
+ result.put("success", true);
+ result.put("data", buildUserData(user));
+ } catch (Exception e) {
+ result.put("success", false);
+ result.put("error", e.getMessage());
+ }
+ return result;
+ }
+
+ @PostMapping("/h5/login")
+ public Map loginH5(@RequestBody Map params) {
+ Map result = new HashMap<>();
+ try {
+ String loginName = params.get("loginName") != null ? params.get("loginName").toString() : "";
+ String password = params.get("password") != null ? params.get("password").toString() : "";
+
+ UserInfo user = userInfoService.loginWithPassword(loginName, password);
+ result.put("success", true);
+ result.put("data", buildUserData(user));
+ } catch (Exception e) {
+ result.put("success", false);
+ result.put("error", e.getMessage());
+ }
+ return result;
+ }
@PostMapping("/updateVip")
public Map updateVipLevel(@RequestBody Map params) {
@@ -407,4 +558,109 @@ public class UserController {
}
return result;
}
+
+ private Map buildUserData(UserInfo user) {
+ Map data = new HashMap<>();
+ data.put("id", user.getId());
+ data.put("userId", user.getUserId());
+ data.put("openId", user.getOpenId());
+ data.put("avatarUrl", user.getAvatarUrl());
+ data.put("username", user.getUsername());
+ data.put("loginName", user.getLoginName());
+ data.put("phone", user.getPhone());
+ data.put("vipLevel", user.getVipLevel());
+ data.put("coins", user.getCoins());
+ data.put("streakDays", user.getStreakDays());
+ data.put("createTime", user.getCreateTime());
+ return data;
+ }
+
+ private ResponseEntity redirect(String targetUrl) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setLocation(URI.create(targetUrl));
+ return new ResponseEntity<>(headers, HttpStatus.FOUND);
+ }
+
+ private String buildFrontendRedirectUrl(String returnUrl, boolean success, String errorMessage) {
+ return buildFrontendRedirectUrl(returnUrl, success, errorMessage, null);
+ }
+
+ private String buildFrontendRedirectUrl(
+ String returnUrl,
+ boolean success,
+ String errorMessage,
+ Map payload
+ ) {
+ Map params = new LinkedHashMap<>();
+ params.put("success", success ? "1" : "0");
+ if (!success && StringUtils.hasText(errorMessage)) {
+ params.put("error", errorMessage);
+ }
+ if (payload != null) {
+ params.putAll(payload);
+ }
+ return appendParamsToUrl(returnUrl, params);
+ }
+
+ private String appendParamsToUrl(String url, Map params) {
+ if (!StringUtils.hasText(url)) {
+ return "/";
+ }
+
+ int hashIndex = url.indexOf('#');
+ if (hashIndex >= 0) {
+ String prefix = url.substring(0, hashIndex);
+ String fragment = url.substring(hashIndex + 1);
+ String separator = fragment.contains("?") ? "&" : "?";
+ return prefix + "#" + fragment + separator + toQueryString(params);
+ }
+
+ String separator = url.contains("?") ? "&" : "?";
+ return url + separator + toQueryString(params);
+ }
+
+ private String toQueryString(Map params) {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry entry : params.entrySet()) {
+ if (entry.getValue() == null) {
+ continue;
+ }
+ if (builder.length() > 0) {
+ builder.append("&");
+ }
+ builder
+ .append(urlEncode(entry.getKey()))
+ .append("=")
+ .append(urlEncode(String.valueOf(entry.getValue())));
+ }
+ return builder.toString();
+ }
+
+ private String urlEncode(String value) {
+ try {
+ return URLEncoder.encode(value, "UTF-8");
+ } catch (Exception e) {
+ throw new IllegalStateException("URL编码失败", e);
+ }
+ }
+
+ private String extractWechatError(Map wxResult, String fallback) {
+ if (wxResult == null) {
+ return fallback;
+ }
+ Object errMsg = wxResult.get("errmsg");
+ if (errMsg != null && StringUtils.hasText(String.valueOf(errMsg))) {
+ return String.valueOf(errMsg);
+ }
+ return fallback;
+ }
+
+ private String resolveRequestBaseUrl(HttpServletRequest request) {
+ return request.getScheme() + "://" + request.getServerName()
+ + (request.getServerPort() == 80 || request.getServerPort() == 443 ? "" : ":" + request.getServerPort());
+ }
+
+ private String resolveDefaultFrontendReturnUrl(HttpServletRequest request) {
+ return resolveRequestBaseUrl(request) + "/#/pages/auth/wechat-callback";
+ }
}
diff --git a/src/main/java/xuan/entity/UserInfo.java b/src/main/java/xuan/entity/UserInfo.java
index 0ae41c4..747a93f 100644
--- a/src/main/java/xuan/entity/UserInfo.java
+++ b/src/main/java/xuan/entity/UserInfo.java
@@ -19,6 +19,12 @@ public class UserInfo {
@Column(length = 50)
private String username;
+
+ @Column(unique = true, length = 50)
+ private String loginName;
+
+ @Column(length = 64)
+ private String passwordHash;
@Column(length = 20)
private String userId;
@@ -65,6 +71,12 @@ public class UserInfo {
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
+
+ public String getLoginName() { return loginName; }
+ public void setLoginName(String loginName) { this.loginName = loginName; }
+
+ public String getPasswordHash() { return passwordHash; }
+ public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
diff --git a/src/main/java/xuan/repository/UserInfoRepository.java b/src/main/java/xuan/repository/UserInfoRepository.java
index c53ad18..843d841 100644
--- a/src/main/java/xuan/repository/UserInfoRepository.java
+++ b/src/main/java/xuan/repository/UserInfoRepository.java
@@ -8,4 +8,5 @@ import xuan.entity.UserInfo;
public interface UserInfoRepository extends JpaRepository {
UserInfo findByOpenId(String openId);
UserInfo findByUserId(String userId);
+ UserInfo findByLoginName(String loginName);
}
diff --git a/src/main/java/xuan/service/UserInfoService.java b/src/main/java/xuan/service/UserInfoService.java
index 3c7dd5f..f3b16eb 100644
--- a/src/main/java/xuan/service/UserInfoService.java
+++ b/src/main/java/xuan/service/UserInfoService.java
@@ -8,6 +8,8 @@ import xuan.entity.CoinConsumeRecord;
import xuan.repository.UserInfoRepository;
import xuan.repository.CoinConsumeRecordRepository;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
import java.util.Random;
@Service
@@ -40,6 +42,53 @@ public class UserInfoService {
return userInfoRepository.save(user);
}
+
+ public UserInfo registerWithPassword(String loginName, String nickname, String password) {
+ if (loginName == null || loginName.trim().isEmpty()) {
+ throw new IllegalArgumentException("请输入用户名");
+ }
+ if (nickname == null || nickname.trim().isEmpty()) {
+ throw new IllegalArgumentException("请输入昵称");
+ }
+ if (password == null || password.length() < 6) {
+ throw new IllegalArgumentException("密码至少6位");
+ }
+
+ String normalizedLoginName = loginName.trim();
+ if (userInfoRepository.findByLoginName(normalizedLoginName) != null) {
+ throw new IllegalArgumentException("用户名已存在");
+ }
+
+ UserInfo user = new UserInfo();
+ user.setOpenId("h5_" + normalizedLoginName);
+ user.setLoginName(normalizedLoginName);
+ user.setPasswordHash(hashPassword(password));
+ user.setUsername(nickname.trim());
+ user.setAvatarUrl("");
+ user.setUserId(generateUserId());
+ user.setVipLevel(0);
+ user.setStreakDays(0);
+
+ return userInfoRepository.save(user);
+ }
+
+ public UserInfo loginWithPassword(String loginName, String password) {
+ if (loginName == null || loginName.trim().isEmpty()) {
+ throw new IllegalArgumentException("请输入用户名");
+ }
+ if (password == null || password.isEmpty()) {
+ throw new IllegalArgumentException("请输入密码");
+ }
+
+ UserInfo user = userInfoRepository.findByLoginName(loginName.trim());
+ if (user == null || user.getPasswordHash() == null) {
+ throw new IllegalArgumentException("用户名或密码错误");
+ }
+ if (!user.getPasswordHash().equals(hashPassword(password))) {
+ throw new IllegalArgumentException("用户名或密码错误");
+ }
+ return user;
+ }
public UserInfo getUserInfo(String userId) {
return userInfoRepository.findByUserId(userId);
@@ -175,4 +224,18 @@ public class UserInfoService {
}
return sb.toString();
}
+
+ private String hashPassword(String password) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] bytes = digest.digest(password.getBytes(StandardCharsets.UTF_8));
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ } catch (Exception e) {
+ throw new IllegalStateException("密码处理失败", e);
+ }
+ }
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 606335f..70f8d81 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -9,7 +9,7 @@ spring.datasource.password=xuan_password
# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
-spring.jpa.hibernate.ddl-auto=create
+spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
@@ -17,5 +17,10 @@ spring.jpa.properties.hibernate.format_sql=true
logging.level.xuan=DEBUG
# WeChat Mini Program Configuration
-wechat.appid=wxbae101ca6e1f61e6
-wechat.secret=26f414d93627acf3e1d895231296b313
+wechat.appid=${WECHAT_APPID:}
+wechat.secret=${WECHAT_SECRET:}
+
+# WeChat H5 QR Login Configuration
+wechat.web.appid=${WECHAT_WEB_APPID:${WECHAT_APPID:}}
+wechat.web.secret=${WECHAT_WEB_SECRET:${WECHAT_SECRET:}}
+wechat.web.callback-base-url=${WECHAT_WEB_CALLBACK_BASE_URL:https://api.bemover.xyz}
diff --git a/xuan-deduct-uniapp/index.html b/xuan-deduct-uniapp/index.html
new file mode 100644
index 0000000..a5cf36a
--- /dev/null
+++ b/xuan-deduct-uniapp/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 排盘工具
+
+
+
+
+
+
diff --git a/xuan-deduct-uniapp/src/components/auth-modal/auth-modal.vue b/xuan-deduct-uniapp/src/components/auth-modal/auth-modal.vue
index 0d84126..91dba80 100644
--- a/xuan-deduct-uniapp/src/components/auth-modal/auth-modal.vue
+++ b/xuan-deduct-uniapp/src/components/auth-modal/auth-modal.vue
@@ -18,13 +18,74 @@
🔐
-
+
+
+
+
+ 注册
+
+
+ 登录
+
+
+
+
+
+
+
+
+
+ {{ h5Mode === 'register' ? '注册后将自动登录,密码至少 6 位。' : '输入用户名和密码登录已有账号。' }}
+
+
+
+
+
+
+
+
-
+
1
@@ -98,6 +159,7 @@
-
-
-
-
-
-
-