Files
quyun-v2/backend/app/services/user.go

241 lines
6.4 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 services
import (
"context"
"encoding/json"
"errors"
"time"
"quyun/v2/app/errorx"
auth_dto "quyun/v2/app/http/v1/dto"
user_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
"quyun/v2/providers/jwt"
"go.ipao.vip/gen/types"
"gorm.io/gorm"
)
// @provider
type user struct {
jwt *jwt.JWT
}
// SendOTP 发送验证码
// 当前仅模拟发送,实际应对接短信服务
func (s *user) SendOTP(ctx context.Context, phone string) error {
// TODO: 对接短信服务
// 模拟发送成功
return nil
}
// LoginWithOTP 手机号验证码登录/注册
func (s *user) LoginWithOTP(ctx context.Context, tenantID int64, phone, otp string) (*auth_dto.LoginResponse, error) {
// 1. 校验验证码 (模拟:固定 123456)
if otp != "1234" {
return nil, errorx.ErrInvalidCredentials.WithMsg("验证码错误")
}
// 2. 查询或创建用户
tbl, query := models.UserQuery.QueryContext(ctx)
u, err := query.Where(tbl.Phone.Eq(phone)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户
u = &models.User{
Phone: phone,
Username: phone, // 默认用户名 = 手机号
Password: "", // 免密登录
Nickname: "User_" + phone[len(phone)-4:],
Status: consts.UserStatusVerified, // 默认已审核
Roles: types.Array[consts.Role]{consts.RoleUser},
Gender: consts.GenderSecret, // 默认性别
}
if err := query.Create(u); err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err).WithMsg("创建用户失败")
}
} else {
return nil, errorx.ErrDatabaseError.WithCause(err).WithMsg("查询用户失败")
}
}
// 3. 检查状态
if u.Status == consts.UserStatusBanned {
return nil, errorx.ErrAccountDisabled
}
// 4. 校验租户成员关系(租户上下文下仅允许成员登录)。
if err := s.ensureTenantMember(ctx, tenantID, u.ID); err != nil {
return nil, err
}
// 5. 生成 Token
token, err := s.jwt.CreateToken(s.jwt.CreateClaims(jwt.BaseClaims{
UserID: u.ID,
TenantID: tenantID,
}))
if err != nil {
return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
}
return &auth_dto.LoginResponse{
Token: token,
User: s.ToAuthUserDTO(u),
}, nil
}
func (s *user) ensureTenantMember(ctx context.Context, tenantID, userID int64) error {
if tenantID <= 0 {
return nil
}
// 校验租户存在避免非法租户ID绕过校验。
tblTenant, tenantQuery := models.TenantQuery.QueryContext(ctx)
tenant, err := tenantQuery.Where(tblTenant.ID.Eq(tenantID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithCause(err).WithMsg("租户不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
if tenant.UserID == userID {
return nil
}
tbl, q := models.TenantUserQuery.QueryContext(ctx)
exists, err := q.Where(
tbl.TenantID.Eq(tenantID),
tbl.UserID.Eq(userID),
tbl.Status.Eq(consts.UserStatusVerified),
).Exists()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if !exists {
return errorx.ErrForbidden.WithMsg("未加入该租户")
}
return nil
}
// GetModelByID 获取指定 ID 的用户model
func (s *user) GetModelByID(ctx context.Context, userID int64) (*models.User, error) {
tbl, query := models.UserQuery.QueryContext(ctx)
u, err := query.Where(tbl.ID.Eq(userID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
return u, nil
}
// Me 获取当前用户信息
func (s *user) Me(ctx context.Context, userID int64) (*auth_dto.User, error) {
u, err := s.GetModelByID(ctx, userID)
if err != nil {
return nil, err
}
return s.ToAuthUserDTO(u), nil
}
// Update 更新用户信息
func (s *user) Update(ctx context.Context, userID int64, form *user_dto.UserUpdate) error {
tbl, query := models.UserQuery.QueryContext(ctx)
_, err := query.Where(tbl.ID.Eq(userID)).Updates(&models.User{
Nickname: form.Nickname,
Avatar: form.Avatar,
Gender: form.Gender,
Bio: form.Bio,
// Birthday: form.Birthday, // 类型转换需处理
})
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
// RealName 实名认证
func (s *user) RealName(ctx context.Context, userID int64, form *user_dto.RealNameForm) error {
// Mock Verification
if len(form.IDCard) != 18 {
return errorx.ErrBadRequest.WithMsg("身份证号格式错误")
}
if form.Realname == "" {
return errorx.ErrBadRequest.WithMsg("真实姓名不能为空")
}
tbl, query := models.UserQuery.QueryContext(ctx)
u, err := query.Where(tbl.ID.Eq(userID)).First()
if err != nil {
return errorx.ErrRecordNotFound
}
var metaMap map[string]interface{}
if len(u.Metas) > 0 {
_ = json.Unmarshal(u.Metas, &metaMap)
}
if metaMap == nil {
metaMap = make(map[string]interface{})
}
// Mock encryption
metaMap["real_name"] = form.Realname
metaMap["id_card"] = "ENC:" + form.IDCard
b, _ := json.Marshal(metaMap)
_, err = query.Where(tbl.ID.Eq(userID)).Updates(&models.User{
IsRealNameVerified: true,
VerifiedAt: time.Now(),
Metas: types.JSON(b),
})
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
// GetNotifications 获取通知
func (s *user) GetNotifications(ctx context.Context, tenantID, userID int64, typeArg string) ([]user_dto.Notification, error) {
tbl, query := models.NotificationQuery.QueryContext(ctx)
query = query.Where(tbl.UserID.Eq(userID))
if tenantID > 0 {
query = query.Where(tbl.TenantID.Eq(tenantID))
}
if typeArg != "" && typeArg != "all" {
query = query.Where(tbl.Type.Eq(typeArg))
}
list, err := query.Order(tbl.CreatedAt.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
result := make([]user_dto.Notification, len(list))
for i, v := range list {
result[i] = user_dto.Notification{
ID: v.ID,
Type: v.Type,
Title: v.Title,
Content: v.Content,
Read: v.IsRead,
Time: v.CreatedAt.Format(time.RFC3339),
}
}
return result, nil
}
func (s *user) ToAuthUserDTO(u *models.User) *auth_dto.User {
return &auth_dto.User{
ID: u.ID,
Phone: u.Phone,
Nickname: u.Nickname,
Avatar: u.Avatar,
Gender: u.Gender, // Direct assignment, types match
Bio: u.Bio,
Balance: float64(u.Balance) / 100.0,
Points: u.Points,
IsRealNameVerified: u.IsRealNameVerified,
}
}