feat: 支持会员卡合同在线签署

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-10-28 12:24:22 +08:00
parent c04a9db627
commit ecea65f596
46 changed files with 4313 additions and 232 deletions

View File

@@ -0,0 +1,14 @@
import { CardTypeCategory } from './enums'
/** 月卡与多次卡属于预付式服务,必须签署书面合同;单次卡和体验卡不签。 */
export function requiresMembershipContract(card: { type: CardTypeCategory | string; totalTimes: number | null }): boolean {
if (card.type === CardTypeCategory.DURATION) return true
if (card.type === CardTypeCategory.TIMES) return card.totalTimes === null || card.totalTimes > 1
return false
}
/** 预约接口返回此文案时,前端引导学员去签署合同。 */
export const CONTRACT_UNSIGNED_MESSAGE = '请先签署会员卡合同后再预约'
export const CONTRACT_REAL_NAME_MAX_LENGTH = 30
export const CONTRACT_SIGNATURE_MAX_BYTES = 300 * 1024

View File

@@ -47,6 +47,18 @@ export enum OrderStatus {
REFUNDED = 'REFUNDED',
}
// ===== Membership Contract =====
export enum MembershipContractStatus {
PENDING_SIGN = 'PENDING_SIGN',
SIGNED = 'SIGNED',
VOIDED = 'VOIDED',
}
export enum MembershipContractSource {
PURCHASE = 'PURCHASE', // 支付后签署
LEGACY = 'LEGACY', // 存量会员卡补签
}
// ===== Invite =====
export enum InviteReferralStatus {
REGISTERED = 'REGISTERED',

View File

@@ -28,6 +28,8 @@ export {
ProfessionalAssessmentKind,
TrainingPlanStatus,
ProgressPhotoAngle,
MembershipContractStatus,
MembershipContractSource,
} from './enums'
// Constants
@@ -150,3 +152,6 @@ export * from './types/body-portrait'
export * from './types/shop'
export * from './types/teaching-case'
export * from './contract'
export * from './types/contract'

View File

@@ -0,0 +1,145 @@
import type { MembershipContractSource, MembershipContractStatus } from '../enums'
export type ContractKind = 'MONTHLY' | 'MULTI_TIMES'
export type ContractPremisesType = 'LEASED' | 'OWNED'
export interface ContractRow {
readonly label: string
readonly value: string
}
export interface ContractClause {
readonly text: string
/** 与学员有重大利害关系的条款,需加粗提示。 */
readonly emphasis?: boolean
}
export interface ContractSection {
readonly title: string
readonly clauses: readonly ContractClause[]
}
/** 渲染后的合同正文;签署时冻结并计算 SHA-256。 */
export interface ContractDocument {
readonly title: string
readonly contractNo: string
readonly templateVersion: string
readonly notice: string
readonly partyA: readonly ContractRow[]
readonly partyB: readonly ContractRow[]
readonly cardRows: readonly ContractRow[]
readonly sections: readonly ContractSection[]
readonly commitment: string
}
export interface ContractSignatureInfo {
readonly signerName: string
readonly signedAt: string
/** data:image/png;base64,... */
readonly memberSignature: string
readonly partyBSignature: string | null
readonly partyBName: string
readonly contentSha256: string
}
export interface MembershipContractSummary {
readonly id: string
readonly contractNo: string
readonly status: MembershipContractStatus
readonly source: MembershipContractSource
readonly requireBeforeBooking: boolean
readonly cardName: string
readonly kind: ContractKind
readonly amount: number
readonly startDate: string
readonly expireDate: string
readonly createdAt: string
readonly signedAt: string | null
readonly voidReason: string | null
}
export interface MembershipContractDetail extends MembershipContractSummary {
readonly partyName: string | null
readonly partyPhone: string | null
readonly document: ContractDocument
readonly contentSha256: string
readonly signature: ContractSignatureInfo | null
/** 乙方信息未配置完成时不能签署。 */
readonly signable: boolean
readonly unsignableReason: string | null
}
export interface AdminMembershipContractRow extends MembershipContractSummary {
readonly user: { readonly id: string; readonly nickname: string; readonly phone: string | null }
readonly partyName: string | null
}
export interface ContractPendingSummary {
readonly pendingCount: number
readonly blockingCount: number
readonly firstPendingId: string | null
}
export interface SaveContractPartyADto {
readonly realName: string
readonly phone: string
}
export interface SignMembershipContractDto {
readonly contentSha256: string
readonly signatureImage: string
readonly agreedTerms: boolean
readonly confirmedHealth: boolean
readonly device?: string
}
export interface ContractPartyConfig {
readonly businessName: string
readonly brandName: string
readonly creditCode: string
readonly operatorName: string
readonly phone: string
readonly address: string
readonly premisesType: ContractPremisesType
readonly leaseEndDate: string | null
/** 单节标准课价(分),用于学员原因退费结算;未配置时取单次卡售价。 */
readonly standardLessonPrice: number
readonly standardLessonPriceConfigured: boolean
readonly signatureImage: string | null
readonly signatureUpdatedAt: string | null
readonly missingFields: readonly string[]
}
export interface UpdateContractPartyDto {
readonly businessName?: string
readonly brandName?: string
readonly creditCode?: string
readonly operatorName?: string
readonly phone?: string
readonly address?: string
readonly premisesType?: ContractPremisesType
readonly leaseEndDate?: string | null
readonly standardLessonPrice?: number
}
export interface LegacyContractCandidate {
readonly membershipId: string
readonly user: { readonly id: string; readonly nickname: string; readonly phone: string | null }
readonly cardName: string
readonly kind: ContractKind
readonly remainingTimes: number | null
readonly totalTimes: number | null
readonly startDate: string
readonly expireDate: string
}
export interface LegacyContractSyncResult {
readonly created: number
readonly needsAmount: readonly LegacyContractCandidate[]
}
export interface CreateLegacyContractDto {
readonly membershipId: string
/** 实付金额(分) */
readonly amount: number
}