feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,12 +13,15 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
||||
homesvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/home"
|
||||
)
|
||||
|
||||
// Service is ops-admin application layer.
|
||||
type Service struct {
|
||||
Repo *repository.AdminRepo
|
||||
Reports *repository.ReportRepo
|
||||
Home *homesvc.Service
|
||||
Scales *repository.ScaleRepo
|
||||
}
|
||||
|
||||
// BootstrapConfig seeds the first admin when table is empty.
|
||||
@@ -121,30 +124,43 @@ func (s *Service) ListUsers(ctx context.Context, q string, limit, offset int) ([
|
||||
return s.Repo.ListUsers(ctx, q, limit, offset)
|
||||
}
|
||||
|
||||
// DashboardStats exposes ops overview.
|
||||
func (s *Service) DashboardStats(ctx context.Context) (*repository.DashboardStats, error) {
|
||||
return s.Repo.GetDashboardStats(ctx)
|
||||
}
|
||||
|
||||
// UserDetail is admin view of one user.
|
||||
type UserDetail struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Profiles []repository.ProfileBrief `json:"profiles"`
|
||||
Membership *repository.MembershipRow `json:"membership"`
|
||||
Orders []repository.OrderListItem `json:"recent_orders"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Nickname *string `json:"nickname,omitempty"`
|
||||
AskPaidQuotaLeft int `json:"ask_paid_quota_left"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Profiles []repository.ProfileBrief `json:"profiles"`
|
||||
Reports []repository.ReportBrief `json:"reports"`
|
||||
Membership *repository.MembershipRow `json:"membership"`
|
||||
Orders []repository.OrderListItem `json:"recent_orders"`
|
||||
}
|
||||
|
||||
// GetUser loads user detail for admin.
|
||||
func (s *Service) GetUser(ctx context.Context, userID uuid.UUID) (*UserDetail, error) {
|
||||
ok, err := s.Repo.UserExists(ctx, userID)
|
||||
phone, nickname, paidLeft, status, createdAt, err := s.Repo.GetUserAccount(ctx, userID)
|
||||
if err != nil {
|
||||
ok, e2 := s.Repo.UserExists(ctx, userID)
|
||||
if e2 != nil {
|
||||
return nil, e2
|
||||
}
|
||||
if !ok {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
profiles, err := s.Repo.ListProfilesForUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
users, err := s.Repo.ListUsers(ctx, userID.String(), 1, 0)
|
||||
if err != nil || len(users) == 0 {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
profiles, err := s.Repo.ListProfilesForUser(ctx, userID)
|
||||
reports, err := s.Repo.ListReportsForUser(ctx, userID, 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,12 +173,16 @@ func (s *Service) GetUser(ctx context.Context, userID uuid.UUID) (*UserDetail, e
|
||||
return nil, err
|
||||
}
|
||||
return &UserDetail{
|
||||
ID: users[0].ID,
|
||||
Status: users[0].Status,
|
||||
CreatedAt: users[0].CreatedAt,
|
||||
Profiles: profiles,
|
||||
Membership: mem,
|
||||
Orders: orders,
|
||||
ID: userID,
|
||||
Status: status,
|
||||
Phone: phone,
|
||||
Nickname: nickname,
|
||||
AskPaidQuotaLeft: paidLeft,
|
||||
CreatedAt: createdAt,
|
||||
Profiles: profiles,
|
||||
Reports: reports,
|
||||
Membership: mem,
|
||||
Orders: orders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -188,6 +208,29 @@ func (s *Service) GrantMembership(ctx context.Context, adminID, userID uuid.UUID
|
||||
return s.Repo.GrantMembershipWithAudit(ctx, adminID, userID, plan, days, meta)
|
||||
}
|
||||
|
||||
// GrantAskQuotaInput adds paid ask replies.
|
||||
type GrantAskQuotaInput struct {
|
||||
Delta int `json:"delta"`
|
||||
}
|
||||
|
||||
var ErrInvalidAskDelta = errString("invalid ask quota delta")
|
||||
|
||||
// GrantAskQuota adds purchased ask quota and audits.
|
||||
func (s *Service) GrantAskQuota(ctx context.Context, adminID, userID uuid.UUID, delta int) (int, error) {
|
||||
if delta <= 0 || delta > 1000 {
|
||||
return 0, ErrInvalidAskDelta
|
||||
}
|
||||
ok, err := s.Repo.UserExists(ctx, userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok {
|
||||
return 0, ErrUserNotFound
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{"delta": delta})
|
||||
return s.Repo.GrantAskQuotaWithAudit(ctx, adminID, userID, delta, meta)
|
||||
}
|
||||
|
||||
// ListOrders lists commerce orders.
|
||||
func (s *Service) ListOrders(ctx context.Context, limit, offset int) ([]repository.OrderListItem, error) {
|
||||
return s.Repo.ListOrders(ctx, nil, limit, offset)
|
||||
|
||||
Reference in New Issue
Block a user