241 lines
6.4 KiB
Go
241 lines
6.4 KiB
Go
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,
|
||
}
|
||
}
|