Files
lunar-mini/server/internal/handler/user.go
T
gouki 50975e0627
Publish Mini Program Dev Version / publish (push) Failing after 11s
Build and Publish Server / build (push) Successful in 2m55s
feat(bazi): 八字页重构——空态示例排盘+字段术语表+文化声明,生辰档案绑定
- 八字页:未绑定生辰时展示虚构示例排盘(1990-08-08 10:30男)与15条字段说明,
  附传统文化声明文案;已绑定时支持多档案切换,排盘增强(十神/藏干/纳音/五行配色/命盘概览)
- 个人中心:新增生辰管理卡片,每用户免费绑定3个出生年月;登录改为真实微信登录
- 服务端:新增 BirthProfile 模型与 /api/user/birth-profiles 增删查接口,AutoMigrate 建表
- 前端:新增 birth-profile.js 本地存储优先+登录后云端双向同步
- 修复:个人中心 tabBar 页面入口误用 navigateTo 改为 switchTab
2026-08-09 23:21:58 +00:00

423 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/gouki/lunar-server/internal/config"
"github.com/gouki/lunar-server/internal/model"
"github.com/gouki/lunar-server/internal/service"
)
// code2Session 调用微信 jscode2session 接口换取 openid
// session_key 仅保存在服务端,绝不下发给客户端
func code2Session(code string) (openID, unionID string, err error) {
cfg := config.Load()
if cfg.Wechat.AppID == "" || cfg.Wechat.AppSecret == "" {
return "", "", fmt.Errorf("wechat appid/secret not configured")
}
query := url.Values{
"appid": {cfg.Wechat.AppID},
"secret": {cfg.Wechat.AppSecret},
"js_code": {code},
"grant_type": {"authorization_code"},
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.weixin.qq.com/sns/jscode2session?" + query.Encode())
if err != nil {
return "", "", fmt.Errorf("jscode2session request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
return "", "", fmt.Errorf("read jscode2session response failed: %w", err)
}
var result struct {
OpenID string `json:"openid"`
UnionID string `json:"unionid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", "", fmt.Errorf("parse jscode2session response failed: %w", err)
}
if result.ErrCode != 0 || result.OpenID == "" {
return "", "", fmt.Errorf("wechat auth failed: errcode=%d errmsg=%s", result.ErrCode, result.ErrMsg)
}
return result.OpenID, result.UnionID, nil
}
// upsertUser 根据 openid 获取用户,不存在则创建
func upsertUser(openID, unionID string) (*model.User, error) {
userService := service.NewUserService()
user, err := userService.GetUserByOpenID(openID)
if err != nil {
return nil, err
}
if user == nil {
user = &model.User{
OpenID: openID,
UnionID: unionID,
Nickname: "微信用户",
Status: 1,
}
if err := userService.CreateUser(user); err != nil {
return nil, err
}
}
return user, nil
}
// UserLogin 用户登录(小程序 wx.login 的 code 换取 token
func UserLogin(c *gin.Context) {
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "参数错误",
})
return
}
openID, unionID, err := code2Session(req.Code)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "微信登录失败",
})
return
}
user, err := upsertUser(openID, unionID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "创建用户失败",
})
return
}
// 禁用用户不允许登录
if user.Status != 1 {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"msg": "账号已被禁用",
})
return
}
cfg := config.Load()
userService := service.NewUserService()
token, err := userService.GenerateToken(user.ID, cfg.JWT.Secret)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "生成token失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{
"token": token,
"user": user,
},
})
}
// UserLogout 用户登出
func UserLogout(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
})
}
// GetUserProfile 获取用户资料
func GetUserProfile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "未授权",
})
return
}
userService := service.NewUserService()
user, err := userService.GetUserByID(userID.(uint))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "获取用户信息失败",
})
return
}
profile, _ := userService.GetUserProfile(user.ID)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{
"user": user,
"profile": profile,
},
})
}
// UpdateUserProfile 更新用户资料
func UpdateUserProfile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "未授权",
})
return
}
var req struct {
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Gender int `json:"gender"`
RealName string `json:"realName"`
Birthday string `json:"birthday"`
BirthTime string `json:"birthTime"`
Zodiac string `json:"zodiac"`
Constellation string `json:"constellation"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "参数错误",
})
return
}
userService := service.NewUserService()
// 更新用户基本信息
user, err := userService.GetUserByID(userID.(uint))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "获取用户信息失败",
})
return
}
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.Gender > 0 {
user.Gender = req.Gender
}
if err := userService.UpdateUser(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "更新用户信息失败",
})
return
}
// 更新用户资料
profile, _ := userService.GetUserProfile(user.ID)
if profile == nil {
profile = &model.UserProfile{
UserID: user.ID,
}
}
if req.RealName != "" {
profile.RealName = req.RealName
}
if req.Birthday != "" {
profile.Birthday = req.Birthday
}
if req.BirthTime != "" {
profile.BirthTime = req.BirthTime
}
if req.Zodiac != "" {
profile.Zodiac = req.Zodiac
}
if req.Constellation != "" {
profile.Constellation = req.Constellation
}
if err := userService.UpdateUserProfile(profile); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "更新用户资料失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{
"user": user,
"profile": profile,
},
})
}
// GetBirthProfiles 获取当前用户的生辰档案列表
func GetBirthProfiles(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "未授权"})
return
}
userService := service.NewUserService()
profiles, err := userService.ListBirthProfiles(userID.(uint))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "获取生辰档案失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{
"profiles": profiles,
"maxFree": model.MaxFreeBirthProfiles,
},
})
}
// CreateBirthProfile 新增生辰档案(每用户免费上限 MaxFreeBirthProfiles 个)
func CreateBirthProfile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "未授权"})
return
}
var req struct {
Name string `json:"name"`
Gender int `json:"gender"`
Birthday string `json:"birthday" binding:"required"`
BirthTime string `json:"birthTime" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误"})
return
}
// 简单格式校验
if _, err := time.Parse("2006-01-02", req.Birthday); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "出生日期格式错误"})
return
}
if _, err := time.Parse("15:04", req.BirthTime); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "出生时间格式错误"})
return
}
if req.Gender != 1 && req.Gender != 2 {
req.Gender = 1
}
profile := &model.BirthProfile{
UserID: userID.(uint),
Name: req.Name,
Gender: req.Gender,
Birthday: req.Birthday,
BirthTime: req.BirthTime,
}
userService := service.NewUserService()
if err := userService.CreateBirthProfile(profile); err != nil {
if err == service.ErrBirthProfileLimit {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "msg": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "新增生辰档案失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{"profile": profile},
})
}
// DeleteBirthProfile 删除生辰档案(仅限本人)
func DeleteBirthProfile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "未授权"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误"})
return
}
userService := service.NewUserService()
if err := userService.DeleteBirthProfile(uint(id), userID.(uint)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "删除生辰档案失败"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success"})
}
// WechatAuth 微信授权(仅返回 openidsession_key 属敏感凭证不下发)
func WechatAuth(c *gin.Context) {
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "参数错误",
})
return
}
openID, _, err := code2Session(req.Code)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "微信授权失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{
"openid": openID,
},
})
}