feat(family): mock 数据层与单测——样例初始化、增删查、上限判定

This commit is contained in:
ozh 2026-09-09 14:50:23 +08:00
parent e88f1f0582
commit 223cd41fec
2 changed files with 304 additions and 0 deletions

190
src/api/family.ts Normal file
View File

@ -0,0 +1,190 @@
import { storage } from '@/utils/storage'
import {
FAMILY_DATA_STORAGE_KEY,
MAX_CARED_PER_FAMILY,
MOCK_ID_SEED
} from '@/constants/family'
export interface Family {
id: string
name: string
memberCount: number
caredCount: number
caredLabel: string
address: string
deviceCount: number
}
export interface EmergencyContact {
id: string
name: string
phone: string
relation: string
}
export interface CaredPerson {
id: string
familyId: string
name: string
idNumber: string
address: string
contacts: EmergencyContact[]
}
export interface FamilyMember {
id: string
familyId: string
name: string
role: 'owner' | 'member'
phone: string
notify: boolean
}
export interface FamilyDetailData {
family: Family
members: FamilyMember[]
cared: CaredPerson[]
}
interface FamilyData {
families: Family[]
cared: CaredPerson[]
members: FamilyMember[]
nextId: number
}
const SEED_DATA: FamilyData = {
families: [
{ id: '846443196293189', name: 'JollyPlace', memberCount: 6, caredCount: 1, caredLabel: 'Jay', address: '--', deviceCount: 24 },
{ id: '735201884550361', name: '我的家庭圈', memberCount: 4, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 1 },
{ id: '628934071266852', name: '康乐之家', memberCount: 1, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 21 },
{ id: '519027468331740', name: '平安庭院', memberCount: 1, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 0 }
],
cared: [
{
id: '900001',
familyId: '846443196293189',
name: 'Jay',
idNumber: '440301195603150000',
address: '深圳市南山区科技园',
contacts: [
{ id: '910001', name: '张女士', phone: '13800138000', relation: '配偶' },
{ id: '910002', name: '李先生', phone: '13900139000', relation: '儿子' }
]
}
],
members: [
{ id: '900001', familyId: '846443196293189', name: 'Jay', role: 'owner', phone: '13800138000', notify: true },
{ id: '900002', familyId: '846443196293189', name: '小美', role: 'member', phone: '13900139000', notify: false }
],
nextId: MOCK_ID_SEED
}
// 读全量数据;storage 为空时写入样例(首次启动初始化)
function loadData(): FamilyData {
const saved = storage.get<FamilyData>(FAMILY_DATA_STORAGE_KEY)
if (saved) {
if (!saved.families || !Array.isArray(saved.families)) {
console.warn('familyData 存储结构异常,回退默认数据')
return resetData()
}
return saved
}
return resetData()
}
function resetData(): FamilyData {
const data: FamilyData = JSON.parse(JSON.stringify(SEED_DATA))
storage.set(FAMILY_DATA_STORAGE_KEY, data)
return data
}
function saveData(data: FamilyData): void {
storage.set(FAMILY_DATA_STORAGE_KEY, data)
}
// mock 异步语义:与真实 API 调用形式一致
const delay = <T>(value: T, ms = 100): Promise<T> =>
new Promise((resolve) => setTimeout(() => resolve(value), ms))
function nextIdOf(data: FamilyData): string {
const id = String(data.nextId)
data.nextId += 1
return id
}
export async function getFamilies(): Promise<Family[]> {
return delay(loadData().families)
}
export async function getFamilyDetail(id: string): Promise<FamilyDetailData | null> {
const data = loadData()
const family = data.families.find((f) => f.id === id)
if (!family) return delay(null)
const members = data.members.filter((m) => m.familyId === id)
const cared = data.cared.filter((c) => c.familyId === id)
return delay({ family, members, cared })
}
export async function addFamily(name: string): Promise<Family> {
const trimmed = name.trim()
if (!trimmed) throw new Error('请输入家庭名')
const data = loadData()
const family: Family = {
id: nextIdOf(data),
name: trimmed,
memberCount: 1,
caredCount: 0,
caredLabel: '--',
address: '--',
deviceCount: 0
}
data.families = [...data.families, family]
saveData(data)
return delay(family)
}
export async function joinFamily(code: string): Promise<Family> {
const trimmed = code.trim()
if (!trimmed) throw new Error('请输入家庭编号')
const data = loadData()
const family: Family = {
id: nextIdOf(data),
name: `加入的 ${trimmed.slice(-4)}`,
memberCount: 2,
caredCount: 0,
caredLabel: '--',
address: '--',
deviceCount: 0
}
data.families = [...data.families, family]
saveData(data)
return delay(family)
}
export async function canAddCared(familyId: string): Promise<boolean> {
const data = loadData()
const count = data.cared.filter((c) => c.familyId === familyId).length
return count < MAX_CARED_PER_FAMILY
}
export async function addCared(input: Omit<CaredPerson, 'id'>): Promise<CaredPerson> {
const data = loadData()
const count = data.cared.filter((c) => c.familyId === input.familyId).length
if (count >= MAX_CARED_PER_FAMILY) throw new Error('已达上限')
const person: CaredPerson = { ...input, id: nextIdOf(data) }
data.cared = [...data.cared, person]
// 同步家庭卡上的关爱对象摘要
const family = data.families.find((f) => f.id === input.familyId)
if (family) {
family.caredCount = count + 1
family.caredLabel = person.name
}
saveData(data)
return delay(person)
}
export async function getCaredByFamily(familyId: string): Promise<CaredPerson[]> {
return delay(loadData().cared.filter((c) => c.familyId === familyId))
}

View File

@ -0,0 +1,114 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
// mock storage:内存 Map 语义,与 Taro.setStorageSync/getStorageSync 等价
const store = new Map<string, unknown>()
vi.mock('@/utils/storage', () => ({
storage: {
get: <T>(key: string): T | null => (store.has(key) ? (store.get(key) as T) : null),
set: <T>(key: string, value: T): void => { store.set(key, value) },
remove: (key: string): void => { store.delete(key) }
}
}))
import {
getFamilies,
getFamilyDetail,
addFamily,
joinFamily,
canAddCared,
addCared,
getCaredByFamily,
type CaredPerson
} from '@/api/family'
import { FAMILY_DATA_STORAGE_KEY } from '@/constants/family'
const caredInput = (over: Partial<CaredPerson> = {}): Omit<CaredPerson, 'id'> => ({
familyId: '735201884550361',
name: '王奶奶',
idNumber: '440301194001010000',
address: '深圳市南山区',
contacts: [],
...over
})
beforeEach(() => {
store.clear()
})
describe('家庭圈 mock 数据层', () => {
it('首次调用 getFamilies 写入样例并返回 4 条', async () => {
const list = await getFamilies()
expect(list).toHaveLength(4)
expect(store.has(FAMILY_DATA_STORAGE_KEY)).toBe(true)
})
it('addFamily 追加家庭且持久化,caredCount 从 0 开始', async () => {
await getFamilies()
const f = await addFamily('测试家庭')
expect(f.name).toBe('测试家庭')
expect(f.caredCount).toBe(0)
const again = await getFamilies()
expect(again).toHaveLength(5)
expect(again.some((x) => x.id === f.id)).toBe(true)
})
it('addFamily 空名抛错', async () => {
await getFamilies()
await expect(addFamily(' ')).rejects.toThrow('请输入家庭名')
})
it('joinFamily 按编号生成新家庭', async () => {
await getFamilies()
const f = await joinFamily('1234567890')
expect(f.name).toBe('加入的 7890')
const again = await getFamilies()
expect(again).toHaveLength(5)
})
it('getFamilyDetail 返回家庭 + 成员 + 关爱对象', async () => {
await getFamilies()
const detail = await getFamilyDetail('846443196293189')
expect(detail.family.name).toBe('JollyPlace')
expect(detail.members.length).toBe(2)
expect(detail.cared.length).toBe(1)
expect(detail.cared[0].name).toBe('Jay')
})
it('getFamilyDetail 不存在返回 null', async () => {
await getFamilies()
expect(await getFamilyDetail('nope')).toBeNull()
})
it('canAddCared 未达上限 true,加满后 false', async () => {
await getFamilies()
// JollyPlace 已有 Jay(1/2),仍未达上限
expect(await canAddCared('846443196293189')).toBe(true)
expect(await canAddCared('735201884550361')).toBe(true)
await addCared(caredInput())
await addCared(caredInput({ name: '李爷爷' }))
expect(await canAddCared('735201884550361')).toBe(false)
})
it('addCared 写入并同步家庭 caredCount/caredLabel', async () => {
await getFamilies()
const saved = await addCared(caredInput())
expect(saved.id).toBeTruthy()
const detail = await getFamilyDetail('735201884550361')
expect(detail.family.caredCount).toBe(1)
expect(detail.family.caredLabel).toBe('王奶奶')
expect(detail.cared[0].contacts).toEqual([])
})
it('addCared 达上限抛错', async () => {
await getFamilies()
await addCared(caredInput())
await addCared(caredInput({ name: '李爷爷' }))
await expect(addCared(caredInput({ name: '第三人' }))).rejects.toThrow('已达上限')
})
it('getCaredByFamily 返回该家庭的关爱对象数组', async () => {
await getFamilies()
const list = await getCaredByFamily('846443196293189')
expect(list.map((c) => c.name)).toEqual(['Jay'])
})
})