feat(auth): 补充《用户服务协议》《隐私政策》内置页与登录/身份证表单勾选同意

微信审核驳回整改:收集身份证号前须明示协议并取得授权同意。
- 新增 pages/agreement 分包协议全文页(user/privacy 两份,含收集目的/方式/用途/存储/删除)
- 登录页、添加关爱对象表单加协议勾选行,未勾选拦截提交,协议名可跳转全文
- 协议页加入游客白名单(登录前需可查看协议)
- 顺带落地上轮未提交的游客入口与手机号行注释
This commit is contained in:
ozh 2026-10-12 11:50:08 +08:00
parent 58561e12e0
commit 1dedbcfa73
11 changed files with 392 additions and 5 deletions

View File

@ -22,6 +22,11 @@ export default {
{ {
root: 'pages/event/feature', root: 'pages/event/feature',
pages: ['detail'] pages: ['detail']
},
{
// 协议全文页:审核要求《用户服务协议》《隐私政策》可查看,内置分包避免外链依赖
root: 'pages/agreement',
pages: ['index']
} }
], ],
window: { window: {

View File

@ -13,5 +13,6 @@ export const ROUTES = Object.freeze({
FAMILY_DETAIL: '/pages/family/detail', FAMILY_DETAIL: '/pages/family/detail',
FAMILY_CONTACT_ADD: '/pages/family/contact/add', FAMILY_CONTACT_ADD: '/pages/family/contact/add',
FAMILY_CONTACT_DETAIL: '/pages/family/contact/detail', FAMILY_CONTACT_DETAIL: '/pages/family/contact/detail',
AGREEMENT: '/pages/agreement/index',
SCENE_SELECT: '/pages/scene/select' SCENE_SELECT: '/pages/scene/select'
}) })

View File

@ -0,0 +1,139 @@
// 协议全文内容:内置小程序分包页展示,避免外部 H5 托管依赖
// 文案需运营/法务最终确认,此处为可过审的完整占位结构(覆盖审核点:收集目的/方式/用途/存储/删除)
export type AgreementType = 'user' | 'privacy'
interface AgreementSection {
heading: string
paragraphs: string[]
}
export interface AgreementDoc {
title: string
updatedAt: string
sections: AgreementSection[]
}
const USER_SECTIONS: AgreementSection[] = [
{
heading: '一、总则',
paragraphs: [
'1.1 欢迎使用智慧养老小程序(以下简称"本小程序")。本协议是您与本小程序运营方之间关于使用本小程序服务所订立的协议。',
'1.2 在使用本小程序前,请您仔细阅读并充分理解本协议及《隐私政策》各条款。您勾选同意或实际使用本小程序服务,即视为您已阅读并同意接受本协议及《隐私政策》的全部内容。'
]
},
{
heading: '二、账号与登录',
paragraphs: [
'2.1 本小程序使用微信授权登录。当您选择登录时,我们将通过微信授权获取您的用户信息(含头像、昵称)用于创建和识别您的账号。',
'2.2 您可以选择不登录,以游客模式浏览本小程序的公开内容。游客模式下部分功能(如设备管理、家庭圈管理)无法使用。'
]
},
{
heading: '三、用户信息的收集与使用',
paragraphs: [
'3.1 为提供家庭圈关爱服务,在您主动添加关爱对象时,我们需要收集关爱对象的姓名、身份证号、关爱地址及紧急联系人信息(姓名、电话、关系),用于身份核验、建立关爱档案及紧急情况下的联系。',
'3.2 为提供设备告警服务,我们需要获取您绑定设备的告警记录,用于向您推送及展示设备告警消息。',
'3.3 上述信息的收集均以实现本服务为目的,具体收集方式、存储与保护措施详见《隐私政策》。'
]
},
{
heading: '四、用户行为规范',
paragraphs: [
'4.1 您承诺在使用本小程序时遵守国家法律法规,不得利用本小程序从事任何违法违规活动。',
'4.2 您应确保所填写信息真实、准确;因填写信息不实导致的后果由您自行承担。'
]
},
{
heading: '五、服务变更与终止',
paragraphs: [
'5.1 我们可能因业务调整、法律法规要求等原因变更、中断或终止部分或全部服务,并将尽可能提前公告。',
'5.2 您可随时停止使用本小程序;您也可按照《隐私政策》所述方式申请删除您的个人信息。'
]
},
{
heading: '六、其他',
paragraphs: [
'6.1 本协议的订立、履行与解释适用中华人民共和国法律。',
'6.2 我们可能适时修订本协议,修订后的协议将在本页面发布。若您继续使用服务,即视为接受修订后的协议。',
'6.3 若您对本协议有任何疑问,可通过小程序内"个人中心"页面提供的渠道与我们联系。'
]
}
]
const PRIVACY_SECTIONS: AgreementSection[] = [
{
heading: '一、引言',
paragraphs: [
'1.1 我们非常重视您的个人信息保护。本政策说明我们如何收集、使用、存储和保护您的个人信息。',
'1.2 请您在使用本小程序前仔细阅读本政策。您勾选同意或实际使用相关功能,即表示您已理解并同意本政策所述内容。'
]
},
{
heading: '二、我们收集的信息与方式',
paragraphs: [
'2.1 微信授权信息:当您选择登录时,通过微信授权方式收集您的头像、昵称,用于创建账号并展示您的身份。此信息在您点击登录并授权时收集。',
'2.2 身份证明信息:当您主动添加关爱对象时,收集关爱对象的姓名、身份证号、关爱地址,用于身份核验与建立关爱档案。此信息仅在您主动填写并提交时收集。',
'2.3 紧急联系人信息:当您主动填写时,收集紧急联系人姓名、电话、关系,用于紧急情况下联系相关人员。',
'2.4 设备与告警信息:当您绑定设备后,收集设备名称、绑定关系及设备产生的告警记录,用于向您提供设备告警服务。'
]
},
{
heading: '三、信息的使用目的',
paragraphs: [
'3.1 创建与管理您的账号,提供登录及身份识别功能;',
'3.2 建立关爱档案,完成关爱对象身份核验;',
'3.3 在紧急情况下联系紧急联系人;',
'3.4 向您展示设备状态与告警消息,保障关爱对象安全;',
'3.5 保障服务安全稳定运行,防止恶意行为。'
]
},
{
heading: '四、信息的存储与保护',
paragraphs: [
'4.1 您的个人信息存储于中华人民共和国境内的服务器。上述信息存储期限为实现本政策所述目的所必需的最短时间,法律法规另有规定的除外。',
'4.2 我们采用加密传输与加密存储等安全措施保护您的身份证号等敏感信息,访问权限严格限制于完成服务所必需的人员。',
'4.3 尽管我们努力保护您的信息,但互联网传输不存在绝对安全,我们无法保证信息的绝对安全。'
]
},
{
heading: '五、信息的共享与委托',
paragraphs: [
'5.1 我们不会向任何第三方出售您的个人信息。',
'5.2 未经您的授权同意,我们不会公开或向第三方提供您的个人信息,但法律法规规定或主管部门强制要求的除外。'
]
},
{
heading: '六、您的权利',
paragraphs: [
'6.1 您有权查询、更正您的个人信息。',
'6.2 您有权删除您的个人信息:您可以删除已添加的关爱对象档案;如需注销账号并删除全部信息,可通过小程序内反馈渠道联系我们,我们将在 15 个工作日内处理。',
'6.3 在您撤回同意或删除信息前,我们不会停止对应服务的基础运行保障。'
]
},
{
heading: '七、未成年人保护',
paragraphs: [
'7.1 本小程序的服务面向成年用户,我们不主动收集未成年人的个人信息。若您是未成年人,请在监护人指导下使用本小程序。'
]
},
{
heading: '八、政策更新',
paragraphs: [
'8.1 我们可能适时修订本政策,修订后将在本页面发布并更新日期。重大变更将通过弹窗等方式提醒您再次确认。'
]
}
]
export const AGREEMENT_TEXTS: Record<AgreementType, AgreementDoc> = {
user: {
title: '用户服务协议',
updatedAt: '2026-10-12',
sections: USER_SECTIONS
},
privacy: {
title: '隐私政策',
updatedAt: '2026-10-12',
sections: PRIVACY_SECTIONS
}
}

View File

@ -0,0 +1,45 @@
// 协议全文页:纯文本阅读场景,白底长文排版
.page {
min-height: 100vh;
background: $color-bg-white;
}
.body {
padding: 24px 40px 60px;
}
.title {
display: block;
font-size: 40px;
font-weight: 700;
color: $color-text;
text-align: center;
}
.updated {
display: block;
margin-top: 12px;
font-size: 24px;
color: $color-text-tertiary;
text-align: center;
}
.section {
margin-top: 40px;
}
.heading {
display: block;
font-size: 30px;
font-weight: 600;
color: $color-text;
}
.paragraph {
display: block;
margin-top: 16px;
font-size: 26px;
line-height: 1.7;
color: $color-text-secondary;
text-align: justify;
}

View File

@ -0,0 +1,31 @@
import { View, Text } from '@tarojs/components'
import { useRouter } from '@tarojs/taro'
import { Navbar } from '@/components/navbar'
import { AGREEMENT_TEXTS, type AgreementType } from './content'
import styles from './index.module.scss'
export default function AgreementPage() {
const router = useRouter()
const raw = router.params.type ?? 'user'
const type: AgreementType = raw === 'privacy' ? 'privacy' : 'user'
const doc = AGREEMENT_TEXTS[type]
return (
<View className={styles.page}>
<Navbar title={doc.title} />
<View className={styles.body}>
<Text className={styles.title}>{doc.title}</Text>
<Text className={styles.updated}>更新日期:{doc.updatedAt}</Text>
{doc.sections.map((s) => (
<View key={s.heading} className={styles.section}>
<Text className={styles.heading}>{s.heading}</Text>
{s.paragraphs.map((p, i) => (
<Text key={i} className={styles.paragraph}>{p}</Text>
))}
</View>
))}
</View>
</View>
)
}

View File

@ -141,3 +141,45 @@
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
} }
// 协议勾选行:收集身份证前须明示协议并取得同意(微信审核整改)
.agreement {
margin: 24px 24px 0;
display: flex;
align-items: flex-start;
}
.checkbox {
width: 36px;
height: 36px;
margin: 4px 12px 0 0;
flex-shrink: 0;
border: 2px solid #c2c7cf;
border-radius: $radius-circle;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.checkboxOn {
background: $color-primary;
border-color: $color-primary;
}
.checkboxTick {
font-size: 24px;
color: #ffffff;
line-height: 1;
}
.agreementText {
flex: 1;
font-size: 24px;
line-height: 1.6;
color: $color-text-secondary;
}
.agreementLink {
color: $color-primary-deep;
}

View File

@ -5,8 +5,9 @@ import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading' import { Loading } from '@/components/loading'
import { saveCareMember, getCareMemberDetail, type CareContact } from '@/api/careMember' import { saveCareMember, getCareMemberDetail, type CareContact } from '@/api/careMember'
import { MAX_EMERGENCY_CONTACTS, MAX_FAMILY_NAME_LEN } from '@/constants/family' import { MAX_EMERGENCY_CONTACTS, MAX_FAMILY_NAME_LEN } from '@/constants/family'
import { navigateBack } from '@/utils/navigation' import { navigateBack, navigateTo } from '@/utils/navigation'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
import { ROUTES } from '@/constants/routes'
import { validateContactForm } from './validate' import { validateContactForm } from './validate'
import styles from './add.module.scss' import styles from './add.module.scss'
@ -34,6 +35,8 @@ export default function ContactAdd() {
const [address, setAddress] = useState('') const [address, setAddress] = useState('')
const [contacts, setContacts] = useState<(CareContact & { tmpId: string })[]>([]) const [contacts, setContacts] = useState<(CareContact & { tmpId: string })[]>([])
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
// 微信审核要求:收集身份证号前需明示协议并取得勾选同意
const [agreed, setAgreed] = useState(false)
// 编辑模式进入时拉取原数据预填 // 编辑模式进入时拉取原数据预填
useDidShow(() => { useDidShow(() => {
@ -61,6 +64,10 @@ export default function ContactAdd() {
const submit = async () => { const submit = async () => {
if (submitting) return if (submitting) return
if (!agreed) {
showToast('请先阅读并同意《用户服务协议》与《隐私政策》')
return
}
const result = validateContactForm({ name, idNumber, address, contacts }) const result = validateContactForm({ name, idNumber, address, contacts })
if (!result.ok) { if (!result.ok) {
showToast(result.error) showToast(result.error)
@ -187,6 +194,24 @@ export default function ContactAdd() {
> >
<Text className={styles.submitText}>{submitting ? '提交中...' : isEdit ? '保存修改' : '确认添加'}</Text> <Text className={styles.submitText}>{submitting ? '提交中...' : isEdit ? '保存修改' : '确认添加'}</Text>
</View> </View>
<View className={styles.agreement} onClick={() => setAgreed((v) => !v)}>
<View className={`${styles.checkbox} ${agreed ? styles.checkboxOn : ''}`}>
{agreed && <Text className={styles.checkboxTick}>✓</Text>}
</View>
<Text className={styles.agreementText}>
我已阅读并同意
<Text
className={styles.agreementLink}
onClick={(e) => { e.stopPropagation(); navigateTo(`${ROUTES.AGREEMENT}?type=user`) }}
>《用户服务协议》</Text>
与
<Text
className={styles.agreementLink}
onClick={(e) => { e.stopPropagation(); navigateTo(`${ROUTES.AGREEMENT}?type=privacy`) }}
>《隐私政策》</Text>
</Text>
</View>
</View> </View>
) )
} }

View File

@ -68,3 +68,65 @@
font-size: 32px; font-size: 32px;
font-weight: 600; font-weight: 600;
} }
// 暂不登录入口:登录非强制,游客可先体验(微信审核要求可取消/拒绝)
.guestEntry {
margin-top: 24px;
display: flex;
justify-content: center;
}
.guestEntryText {
font-size: 26px;
color: $color-text-secondary;
}
// 协议勾选行:登录收集微信用户信息前须明示协议(微信审核整改)
.agreement {
margin-top: 24px;
display: flex;
align-items: flex-start;
}
.checkbox {
width: 36px;
height: 36px;
margin: 4px 12px 0 0;
flex-shrink: 0;
border: 2px solid #c2c7cf;
border-radius: $radius-circle;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.checkboxOn {
background: $color-primary;
border-color: $color-primary;
}
.checkboxTick {
font-size: 24px;
color: #ffffff;
line-height: 1;
}
.agreementTextWrap {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.agreementText {
font-size: 24px;
line-height: 1.6;
color: $color-text-secondary;
}
.agreementLink {
font-size: 24px;
line-height: 1.6;
color: $color-primary-deep;
}

View File

@ -7,7 +7,7 @@ import { performWxLogin, completeUserInfoLogin, getLoginSubState } from './wxLog
import { getLoginInfo } from '@/api/auth' import { getLoginInfo } from '@/api/auth'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { reLaunch } from '@/utils/navigation' import { reLaunch, navigateTo } from '@/utils/navigation'
import { storage } from '@/utils/storage' import { storage } from '@/utils/storage'
import { WX_OPENID_STORAGE_KEY } from '@/constants/auth' import { WX_OPENID_STORAGE_KEY } from '@/constants/auth'
import styles from './index.module.scss' import styles from './index.module.scss'
@ -23,6 +23,8 @@ export default function Login() {
const [openId, setOpenId] = useState('') const [openId, setOpenId] = useState('')
// 冷启动校验进行中:阻止 useDidShow 与按钮登录并发 // 冷启动校验进行中:阻止 useDidShow 与按钮登录并发
const [restoring, setRestoring] = useState(false) const [restoring, setRestoring] = useState(false)
// 微信审核要求:登录收集微信用户信息前需勾选同意协议
const [agreed, setAgreed] = useState(false)
// 登录后落地页:携带 redirect 参数时优先回跳(守卫踢登录/分享卡直达场景),否则进首页 // 登录后落地页:携带 redirect 参数时优先回跳(守卫踢登录/分享卡直达场景),否则进首页
const afterLoginUrl = () => { const afterLoginUrl = () => {
@ -72,6 +74,10 @@ export default function Login() {
const handleWxLogin = async () => { const handleWxLogin = async () => {
if (loading || restoring) return if (loading || restoring) return
if (!agreed) {
toast.error('请先阅读并同意《用户服务协议》与《隐私政策》')
return
}
setLoading(true) setLoading(true)
try { try {
applyResult(await performWxLogin()) applyResult(await performWxLogin())
@ -87,6 +93,10 @@ export default function Login() {
// getUserProfile 的资料后端不认;getUserInfo 不受 tap 上下文限制,授权弹窗由微信拉起) // getUserProfile 的资料后端不认;getUserInfo 不受 tap 上下文限制,授权弹窗由微信拉起)
const handleAuthUserInfo = async () => { const handleAuthUserInfo = async () => {
if (loading) return if (loading) return
if (!agreed) {
toast.error('请先阅读并同意《用户服务协议》与《隐私政策》')
return
}
setLoading(true) setLoading(true)
try { try {
applyResult(await completeUserInfoLogin(openId || undefined)) applyResult(await completeUserInfoLogin(openId || undefined))
@ -116,6 +126,30 @@ export default function Login() {
{loading ? '登录中...' : restoring ? '恢复登录...' : needAuth ? '授权用户信息,继续' : '微信一键登录'} {loading ? '登录中...' : restoring ? '恢复登录...' : needAuth ? '授权用户信息,继续' : '微信一键登录'}
</Text> </Text>
</View> </View>
{/* 协议勾选:登录收集微信用户信息前须取得同意(微信审核整改) */}
<View className={styles.agreement}>
<View className={`${styles.checkbox} ${agreed ? styles.checkboxOn : ''}`} onClick={() => setAgreed((v) => !v)}>
{agreed && <Text className={styles.checkboxTick}>✓</Text>}
</View>
<View className={styles.agreementTextWrap} onClick={() => setAgreed((v) => !v)}>
<Text className={styles.agreementText}>我已阅读并同意</Text>
<Text
className={styles.agreementLink}
onClick={(e) => { e.stopPropagation(); navigateTo(`${ROUTES.AGREEMENT}?type=user`) }}
>《用户服务协议》</Text>
<Text className={styles.agreementText}>与</Text>
<Text
className={styles.agreementLink}
onClick={(e) => { e.stopPropagation(); navigateTo(`${ROUTES.AGREEMENT}?type=privacy`) }}
>《隐私政策》</Text>
</View>
</View>
{/* 游客浏览入口:登录非强制(微信审核要求可取消/拒绝登录) */}
<View className={styles.guestEntry} onClick={() => reLaunch(ROUTES.HOME)}>
<Text className={styles.guestEntryText}>暂不登录,先逛逛</Text>
</View>
</View> </View>
</View> </View>
) )

View File

@ -54,7 +54,8 @@ export default function Mine() {
<ListRowGroup> <ListRowGroup>
<ListRow label="配置设备" arrow onClick={() => navigateTo(ROUTES.DEVICE_BLE_CONFIG)} /> <ListRow label="配置设备" arrow onClick={() => navigateTo(ROUTES.DEVICE_BLE_CONFIG)} />
<ListRow label="场所管理" arrow onClick={() => navigateTo(ROUTES.SCENE_SELECT)} /> <ListRow label="场所管理" arrow onClick={() => navigateTo(ROUTES.SCENE_SELECT)} />
<ListRow label="手机号" value={user?.mobile || '未绑定'} arrow /> {/* 手机号绑定流程暂未开通,先隐藏入口(mobile 接口字段待后端确认) */}
{/* <ListRow label="手机号" value={user?.mobile || '未绑定'} arrow /> */}
<ListRow label="当前版本" value="2.8.44" /> <ListRow label="当前版本" value="2.8.44" />
<ListRow label="退出登录" arrow danger onClick={logout} /> <ListRow label="退出登录" arrow danger onClick={logout} />
</ListRowGroup> </ListRowGroup>

View File

@ -6,13 +6,15 @@ import { ROUTES } from '@/constants/routes'
// 故不用全局拦截器,改为在 navigation 统一出口做跳转前校验。 // 故不用全局拦截器,改为在 navigation 统一出口做跳转前校验。
// 游客可浏览页面(微信审核要求:进入首页即要求授权登录会被拒, // 游客可浏览页面(微信审核要求:进入首页即要求授权登录会被拒,
// tab 页与登录页放开,操作型页面按需登录) // tab 页与登录页放开,操作型页面按需登录;协议全文页游客可读——
// 登录前需可查看协议内容才能完成勾选同意)
export const PUBLIC_PAGES = new Set<string>([ export const PUBLIC_PAGES = new Set<string>([
ROUTES.LOGIN, ROUTES.LOGIN,
ROUTES.HOME, ROUTES.HOME,
ROUTES.DEVICE_LIST, ROUTES.DEVICE_LIST,
ROUTES.EVENT, ROUTES.EVENT,
ROUTES.MINE ROUTES.MINE,
ROUTES.AGREEMENT
]) ])
// 登录页路径(navigation 守卫改投目标) // 登录页路径(navigation 守卫改投目标)