feat: 新增个人身体画像评估与馆主经营助手

把 3 分钟身体状态评估做成独立获客链路,匿名测评后登录认领完整报告,并接入体验预约与今日待办。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-11 22:09:19 +08:00
parent b8c0dd6781
commit ab6602e41d
56 changed files with 4640 additions and 25 deletions

View File

@@ -62,3 +62,171 @@ export enum SubscriptionMessageScene {
CLASS_REMINDER = 'CLASS_REMINDER',
ADMIN_BOOKING_CREATED = 'ADMIN_BOOKING_CREATED',
}
// ===== Body portrait =====
export enum BodyPortraitSource {
XHS = 'xhs',
WECHAT = 'wechat',
DIANPING = 'dianping',
MEMBER_SHARE = 'member_share',
ORGANIC = 'organic',
OTHER = 'other',
}
export enum BodyPortraitAssessmentStatus {
DRAFT = 'DRAFT',
COMPLETED = 'COMPLETED',
CLAIMED = 'CLAIMED',
EXPIRED = 'EXPIRED',
}
export enum BodyRegion {
NECK = 'neck',
SHOULDER = 'shoulder',
UPPER_BACK = 'upperBack',
LOW_BACK = 'lowBack',
PELVIS = 'pelvis',
HIP = 'hip',
KNEE = 'knee',
LEG = 'leg',
}
export enum PortraitGoal {
POSTURE = 'posture',
NECK_RELIEF = 'neckRelief',
BACK_COMFORT = 'backComfort',
CORE = 'core',
SHAPING = 'shaping',
HIP_LEG = 'hipLeg',
STABILITY = 'stability',
OTHER = 'other',
}
export enum SittingHours {
LT4 = 'lt4',
H4_TO_6 = 'h4to6',
H6_TO_8 = 'h6to8',
GT8 = 'gt8',
}
export enum ExerciseFreq {
NONE = 'none',
W1_TO_2 = 'w1to2',
W3_TO_4 = 'w3to4',
W5_PLUS = 'w5plus',
}
export enum WorkPosture {
COMPUTER = 'computer',
PHONE = 'phone',
STANDING = 'standing',
WALKING = 'walking',
HOLDING_CHILD = 'holdingChild',
OTHER = 'other',
}
export enum AfterSitting {
NECK_TIGHT = 'neckTight',
LOW_BACK_ACHE = 'lowBackAche',
BACK_STIFF = 'backStiff',
HIP_TIGHT = 'hipTight',
NONE = 'none',
}
export enum StandingNotice {
HEAD_FORWARD = 'headForward',
ROUNDED_SHOULDERS = 'roundedShoulders',
UNEVEN_SHOULDERS = 'unevenShoulders',
BELLY_FORWARD = 'bellyForward',
LOCKED_KNEES = 'lockedKnees',
UNNOTICED = 'unnoticed',
}
export enum SafetyFlag {
PERSISTENT_PAIN = 'persistentPain',
RECENT_INJURY = 'recentInjury',
NUMBNESS = 'numbness',
DOCTOR_LIMIT = 'doctorLimit',
MEDICAL_RECOVERY = 'medicalRecovery',
NONE = 'none',
}
export enum BodyDimension {
CERVICAL_SHOULDER = 'cervicalShoulder',
SPINAL_MOBILITY = 'spinalMobility',
CORE_CONTROL = 'coreControl',
HIP_PELVIS = 'hipPelvis',
LOWER_LIMB = 'lowerLimb',
}
export enum ConcernLevel {
LOW = 'low',
MILD = 'mild',
MODERATE = 'moderate',
HIGH = 'high',
PRIORITY = 'priority',
}
export enum PortraitType {
SEDENTARY_NECK_TENSION = 'sedentaryNeckTension',
SEDENTARY_CORE_WEAK = 'sedentaryCoreWeak',
NECK_COMPENSATION = 'neckCompensation',
HIP_TIGHT = 'hipTight',
UPPER_LOWER_CROSS = 'upperLowerCross',
LOWER_LIMB_UNSTABLE = 'lowerLimbUnstable',
MIXED_FATIGUE = 'mixedFatigue',
LOW_OVERALL_CONCERN = 'lowOverallConcern',
}
export enum PortraitMatchQuality {
FULL = 'full',
PARTIAL = 'partial',
}
export enum GrowthLeadStage {
VISIT = 'VISIT',
STARTED = 'STARTED',
COMPLETED = 'COMPLETED',
CLAIMED = 'CLAIMED',
PHONE_BOUND = 'PHONE_BOUND',
TRIAL_PURCHASED = 'TRIAL_PURCHASED',
TRIAL_BOOKED = 'TRIAL_BOOKED',
TRIAL_ATTENDED = 'TRIAL_ATTENDED',
PLAN_PURCHASED = 'PLAN_PURCHASED',
TRAINING = 'TRAINING',
RENEWAL_DUE = 'RENEWAL_DUE',
RENEWED = 'RENEWED',
}
export enum GrowthEventName {
ASSESSMENT_STARTED = 'assessment_started',
ASSESSMENT_COMPLETED = 'assessment_completed',
ASSESSMENT_CLAIMED = 'assessment_claimed',
REPORT_VIEWED = 'report_viewed',
ADVICE_VIEWED = 'advice_viewed',
PHONE_BOUND = 'phone_bound',
TRIAL_CLICKED = 'trial_clicked',
TRIAL_PURCHASED = 'trial_purchased',
TRIAL_BOOKED = 'trial_booked',
TRIAL_ATTENDED = 'trial_attended',
PLAN_PURCHASED = 'plan_purchased',
REPORT_SHARED = 'report_shared',
}
export enum ProfessionalAssessmentKind {
INITIAL = 'INITIAL',
FOLLOW_UP = 'FOLLOW_UP',
}
export enum TrainingPlanStatus {
DRAFT = 'DRAFT',
ACTIVE = 'ACTIVE',
COMPLETED = 'COMPLETED',
}
export enum ProgressPhotoAngle {
FRONT = 'FRONT',
SIDE = 'SIDE',
BACK = 'BACK',
DETAIL = 'DETAIL',
}

View File

@@ -9,6 +9,25 @@ export {
OrderStatus,
InviteReferralStatus,
SubscriptionMessageScene,
BodyPortraitSource,
BodyPortraitAssessmentStatus,
BodyRegion,
PortraitGoal,
SittingHours,
ExerciseFreq,
WorkPosture,
AfterSitting,
StandingNotice,
SafetyFlag,
BodyDimension,
ConcernLevel,
PortraitType,
PortraitMatchQuality,
GrowthLeadStage,
GrowthEventName,
ProfessionalAssessmentKind,
TrainingPlanStatus,
ProgressPhotoAngle,
} from './enums'
// Constants
@@ -116,3 +135,4 @@ export type {
} from './types/index'
export * from './types/member-care'
export * from './types/body-portrait'

View File

@@ -0,0 +1,427 @@
import {
AfterSitting,
BodyDimension,
BodyPortraitAssessmentStatus,
BodyPortraitSource,
BodyRegion,
ConcernLevel,
ExerciseFreq,
GrowthEventName,
GrowthLeadStage,
PortraitGoal,
PortraitMatchQuality,
PortraitType,
ProfessionalAssessmentKind,
ProgressPhotoAngle,
SafetyFlag,
SittingHours,
StandingNotice,
TrainingPlanStatus,
WorkPosture,
} from '../enums'
export const BODY_PORTRAIT_QUESTIONNAIRE_VERSION = 'portrait-q-v1'
export const BODY_PORTRAIT_RULES_VERSION = 'portrait-r-v1'
export const BODY_PORTRAIT_OFFLINE_PROTOCOL = 'offline-v1'
export const BODY_PORTRAIT_DISCLAIMER =
'本评估不是医疗诊断或体态疾病检测,仅用于运动训练参考。若你正处于明显疼痛、伤病或医生限制运动的阶段,请先由医生或相应专业人士确认运动条件。'
export const BODY_REGION_LABELS: Record<BodyRegion, string> = {
[BodyRegion.NECK]: '头颈',
[BodyRegion.SHOULDER]: '肩',
[BodyRegion.UPPER_BACK]: '上背',
[BodyRegion.LOW_BACK]: '腰',
[BodyRegion.PELVIS]: '骨盆',
[BodyRegion.HIP]: '髋',
[BodyRegion.KNEE]: '膝',
[BodyRegion.LEG]: '腿',
}
export const PORTRAIT_GOAL_LABELS: Record<PortraitGoal, string> = {
[PortraitGoal.POSTURE]: '看起来更挺拔',
[PortraitGoal.NECK_RELIEF]: '肩颈没那么累',
[PortraitGoal.BACK_COMFORT]: '久坐更舒服',
[PortraitGoal.CORE]: '腹部更有力量',
[PortraitGoal.SHAPING]: '身体线条更好',
[PortraitGoal.HIP_LEG]: '腿型/髋部状态改善',
[PortraitGoal.STABILITY]: '动作更稳定',
[PortraitGoal.OTHER]: '我有自己的目标',
}
export const SITTING_HOURS_LABELS: Record<SittingHours, string> = {
[SittingHours.LT4]: '不到 4 小时',
[SittingHours.H4_TO_6]: '46 小时',
[SittingHours.H6_TO_8]: '68 小时',
[SittingHours.GT8]: '超过 8 小时',
}
export const EXERCISE_FREQ_LABELS: Record<ExerciseFreq, string> = {
[ExerciseFreq.NONE]: '几乎不运动',
[ExerciseFreq.W1_TO_2]: '12 次/周',
[ExerciseFreq.W3_TO_4]: '34 次/周',
[ExerciseFreq.W5_PLUS]: '5 次以上',
}
export const WORK_POSTURE_LABELS: Record<WorkPosture, string> = {
[WorkPosture.COMPUTER]: '电脑办公',
[WorkPosture.PHONE]: '低头手机',
[WorkPosture.STANDING]: '久站',
[WorkPosture.WALKING]: '频繁走动',
[WorkPosture.HOLDING_CHILD]: '抱娃',
[WorkPosture.OTHER]: '其他',
}
export const AFTER_SITTING_LABELS: Record<AfterSitting, string> = {
[AfterSitting.NECK_TIGHT]: '肩颈紧',
[AfterSitting.LOW_BACK_ACHE]: '腰酸',
[AfterSitting.BACK_STIFF]: '背部僵硬',
[AfterSitting.HIP_TIGHT]: '臀腿紧',
[AfterSitting.NONE]: '没明显感觉',
}
export const STANDING_NOTICE_LABELS: Record<StandingNotice, string> = {
[StandingNotice.HEAD_FORWARD]: '头容易往前',
[StandingNotice.ROUNDED_SHOULDERS]: '肩膀向内',
[StandingNotice.UNEVEN_SHOULDERS]: '左右肩不一样高',
[StandingNotice.BELLY_FORWARD]: '肚子容易往前顶',
[StandingNotice.LOCKED_KNEES]: '膝盖容易锁死',
[StandingNotice.UNNOTICED]: '没有注意过',
}
export const SAFETY_FLAG_LABELS: Record<SafetyFlag, string> = {
[SafetyFlag.PERSISTENT_PAIN]: '持续明显疼痛',
[SafetyFlag.RECENT_INJURY]: '最近受伤',
[SafetyFlag.NUMBNESS]: '手脚明显麻木',
[SafetyFlag.DOCTOR_LIMIT]: '医生要求限制运动',
[SafetyFlag.MEDICAL_RECOVERY]: '正处于需要专业医疗评估的恢复阶段',
[SafetyFlag.NONE]: '以上都没有',
}
export const BODY_DIMENSION_LABELS: Record<BodyDimension, string> = {
[BodyDimension.CERVICAL_SHOULDER]: '肩颈状态',
[BodyDimension.SPINAL_MOBILITY]: '脊柱活动',
[BodyDimension.CORE_CONTROL]: '核心控制',
[BodyDimension.HIP_PELVIS]: '髋骨盆状态',
[BodyDimension.LOWER_LIMB]: '下肢稳定',
}
export const CONCERN_LEVEL_LABELS: Record<ConcernLevel, string> = {
[ConcernLevel.LOW]: '低关注',
[ConcernLevel.MILD]: '轻度关注',
[ConcernLevel.MODERATE]: '中度关注',
[ConcernLevel.HIGH]: '高关注',
[ConcernLevel.PRIORITY]: '重点关注',
}
export const PORTRAIT_TYPE_LABELS: Record<PortraitType, string> = {
[PortraitType.SEDENTARY_NECK_TENSION]: '久坐肩颈紧张型',
[PortraitType.SEDENTARY_CORE_WEAK]: '久坐核心弱化型',
[PortraitType.NECK_COMPENSATION]: '肩颈代偿型',
[PortraitType.HIP_TIGHT]: '髋部紧张型',
[PortraitType.UPPER_LOWER_CROSS]: '上下交叉混合型',
[PortraitType.LOWER_LIMB_UNSTABLE]: '下肢稳定不足型',
[PortraitType.MIXED_FATIGUE]: '全身疲劳累积型',
[PortraitType.LOW_OVERALL_CONCERN]: '整体关注度较低型',
}
export const GROWTH_LEAD_STAGE_LABELS: Record<GrowthLeadStage, string> = {
[GrowthLeadStage.VISIT]: '访问',
[GrowthLeadStage.STARTED]: '开始测评',
[GrowthLeadStage.COMPLETED]: '完成报告',
[GrowthLeadStage.CLAIMED]: '已认领',
[GrowthLeadStage.PHONE_BOUND]: '已留联系方式',
[GrowthLeadStage.TRIAL_PURCHASED]: '已购体验',
[GrowthLeadStage.TRIAL_BOOKED]: '已预约',
[GrowthLeadStage.TRIAL_ATTENDED]: '已到店',
[GrowthLeadStage.PLAN_PURCHASED]: '已购买计划',
[GrowthLeadStage.TRAINING]: '训练中',
[GrowthLeadStage.RENEWAL_DUE]: '待续费',
[GrowthLeadStage.RENEWED]: '已续费',
}
export interface BodyPortraitAnswers {
readonly concerns: readonly BodyRegion[]
readonly goal: PortraitGoal | null
readonly sittingHours: SittingHours | null
readonly exerciseFreq: ExerciseFreq | null
readonly workPosture: WorkPosture | null
readonly endOfDayFatigue: readonly BodyRegion[]
readonly afterSitting: readonly AfterSitting[]
readonly standingNotice: readonly StandingNotice[]
readonly safety: readonly SafetyFlag[]
}
export interface BodyPortraitScores {
readonly cervicalShoulder: number
readonly spinalMobility: number
readonly coreControl: number
readonly hipPelvis: number
readonly lowerLimb: number
}
export interface BodyPortraitLevels {
readonly cervicalShoulder: ConcernLevel
readonly spinalMobility: ConcernLevel
readonly coreControl: ConcernLevel
readonly hipPelvis: ConcernLevel
readonly lowerLimb: ConcernLevel
}
export interface BodyPortraitEvidence {
readonly dimension: BodyDimension
readonly title: string
readonly answers: readonly string[]
readonly explanation: string
}
export interface BodyPortraitAdviceItem {
readonly title: string
readonly detail: string
readonly stopIf: string
}
export interface BodyPortraitAdvice {
readonly id: string
readonly items: readonly BodyPortraitAdviceItem[]
}
export interface BodyPortraitChain {
readonly steps: readonly string[]
readonly takeaway: string
}
export interface BodyPortraitSafetyResult {
readonly flagged: boolean
readonly flags: readonly SafetyFlag[]
readonly message: string | null
}
export interface BodyPortraitReport {
readonly questionnaireVersion: string
readonly rulesVersion: string
readonly scores: BodyPortraitScores
readonly levels: BodyPortraitLevels
readonly primaryType: PortraitType
readonly secondaryType: PortraitType | null
readonly headline: string
readonly summary: string
readonly evidence: readonly BodyPortraitEvidence[]
readonly chain: BodyPortraitChain
readonly advice: BodyPortraitAdvice | null
readonly matchQuality: PortraitMatchQuality
readonly safety: BodyPortraitSafetyResult
readonly firstAssessmentHints: readonly string[]
readonly goalLabel: string | null
}
export interface BodyPortraitTeaser {
readonly assessmentId: string
readonly headline: string
readonly primaryType: PortraitType
readonly primaryTypeLabel: string
readonly topDimension: BodyDimension | null
readonly topDimensionLabel: string | null
readonly matchQuality: PortraitMatchQuality
readonly safetyFlagged: boolean
readonly disclaimer: string
}
export interface BodyPortraitVisitResponse {
readonly visitId: string
readonly visitToken: string
readonly source: BodyPortraitSource
}
export interface BodyPortraitSessionResponse {
readonly assessmentId: string
readonly accessToken: string
readonly visitToken: string
readonly questionnaireVersion: string
readonly status: BodyPortraitAssessmentStatus
readonly answers: BodyPortraitAnswers
readonly teaser: BodyPortraitTeaser | null
readonly report: BodyPortraitReport | null
readonly claimed: boolean
}
export interface BodyPortraitStats {
readonly completedCount: number
}
export interface CreateBodyPortraitVisitDto {
readonly source?: BodyPortraitSource | string
readonly campaignId?: string
readonly referralCode?: string
readonly landingPath?: string
}
export interface SaveBodyPortraitAnswersDto {
readonly accessToken: string
readonly answers: Partial<BodyPortraitAnswers>
}
export interface BodyPortraitAccessDto {
readonly accessToken: string
}
export interface TrackGrowthEventDto {
readonly name: GrowthEventName
readonly accessToken?: string
readonly idempotencyKey?: string
}
export interface GrowthEventRecord {
readonly id: string
readonly name: GrowthEventName
readonly occurredAt: string
}
export interface GrowthLeadSummary {
readonly id: string
readonly userId: string
readonly nickname: string
readonly phone: string | null
readonly stage: GrowthLeadStage
readonly source: BodyPortraitSource | null
readonly campaignId: string | null
readonly primaryType: PortraitType | null
readonly topDimension: BodyDimension | null
readonly safetyFlagged: boolean
readonly lastActiveAt: string
readonly completedAt: string | null
readonly followUpDraft: string
}
export interface GrowthLeadDetail extends GrowthLeadSummary {
readonly goalLabel: string | null
readonly scores: BodyPortraitScores | null
readonly levels: BodyPortraitLevels | null
readonly evidence: readonly BodyPortraitEvidence[]
readonly answers: BodyPortraitAnswers | null
readonly report: BodyPortraitReport | null
readonly events: readonly GrowthEventRecord[]
readonly coachNote: string
readonly firstAssessmentHints: readonly string[]
}
export interface GrowthTodayTask {
readonly kind: 'completed_unbooked' | 'trial_unpurchased' | 'reassessment_due'
readonly leadId: string
readonly userId: string
readonly nickname: string
readonly title: string
readonly detail: string
readonly happenedAt: string
readonly followUpDraft: string
}
export interface GrowthFunnelRow {
readonly source: BodyPortraitSource
readonly started: number
readonly completed: number
readonly claimed: number
readonly booked: number
readonly attended: number
readonly purchased: number
}
export interface GrowthTodayDashboard {
readonly tasks: readonly GrowthTodayTask[]
readonly funnel: readonly GrowthFunnelRow[]
}
export interface ProfessionalObservationScores {
readonly headPosition: number
readonly shoulderPosition: number
readonly thoracicExtension: number
readonly shoulderFlexion: number
readonly breathing: number
readonly pelvis: number
readonly coreControl: number
readonly hipMobility: number
readonly singleLeg: number
}
export interface ProfessionalAssessmentSessionRecord {
readonly id: string
readonly userId: string
readonly kind: ProfessionalAssessmentKind
readonly protocolVersion: string
readonly recordedAt: string
readonly bookingId: string | null
readonly originAssessmentId: string | null
readonly observations: ProfessionalObservationScores
readonly subjectiveTension: number | null
readonly coachSummary: string
readonly trainingFocus: string
readonly phaseGoal: string
readonly photoAngle: ProgressPhotoAngle | null
}
export interface CreateProfessionalAssessmentDto {
readonly kind: ProfessionalAssessmentKind
readonly recordedAt: string
readonly bookingId?: string
readonly originAssessmentId?: string
readonly observations: ProfessionalObservationScores
readonly subjectiveTension?: number | null
readonly coachSummary: string
readonly trainingFocus: string
readonly phaseGoal: string
readonly photoAngle?: ProgressPhotoAngle | null
}
export interface TrainingPlanPhaseRecord {
readonly id: string
readonly name: string
readonly lessonStart: number
readonly lessonEnd: number
readonly focus: readonly string[]
readonly summary: string
}
export interface TrainingPlanRecord {
readonly id: string
readonly userId: string
readonly title: string
readonly status: TrainingPlanStatus
readonly weeks: number
readonly sessionId: string | null
readonly phases: readonly TrainingPlanPhaseRecord[]
}
export interface CreateTrainingPlanDto {
readonly sessionId?: string
readonly title?: string
readonly weeks?: number
}
export interface ReassessmentTodoRecord {
readonly id: string
readonly userId: string
readonly planId: string
readonly lessonCheckpoint: number
readonly dueAt: string
readonly completedAt: string | null
}
export interface GrowthComparisonRow {
readonly label: string
readonly first: string
readonly latest: string
}
export interface GrowthShareCardRecord {
readonly id: string
readonly shareCode: string
readonly title: string
readonly caption: string
readonly completedCount: number
readonly weeks: number
readonly rows: readonly GrowthComparisonRow[]
readonly includePhotos: boolean
}
export interface CreateGrowthShareCardDto {
readonly includePhotos?: boolean
}

View File

@@ -10,6 +10,7 @@ export interface Booking {
readonly confirmedAt: string | null
readonly completedAt: string | null
readonly operatorId: string | null
readonly originAssessmentId?: string | null
readonly createdAt: string
readonly updatedAt: string
}
@@ -69,6 +70,7 @@ export interface BookingStatusHistory {
export interface CreateBookingDto {
readonly timeSlotId: string
readonly membershipId: string
readonly originAssessmentId?: string
}
export interface AdminArrangeBookingDto {

View File

@@ -53,3 +53,4 @@ export type {
} from './invite'
export type { TeachingAnalytics, TeachingAnalyticsRecord, TeachingAnalyticsSummary } from './teaching-analytics'
export * from './body-portrait'

View File

@@ -16,7 +16,7 @@ export interface MemberNoteRecord {
booking: { timeSlot: { date: string; startTime: string } } | null
}
export interface ProgressPhotoRecord {
id: string; caption: string; recordedAt: string; consentedAt: string | null; revokedAt: string | null
id: string; caption: string; recordedAt: string; consentedAt: string | null; revokedAt: string | null; angle?: string | null; sessionId?: string | null
}
export interface ProgressArchive {
nickname: string; completedCount: number; milestones: number[]; metrics: BodyMetricRecord[]; notes: MemberNoteRecord[]; photos: ProgressPhotoRecord[]