feat: collect customer identity for assisted bookings

This commit is contained in:
malei 2026-08-01 20:31:53 +08:00
parent f192d89534
commit ec4015aaba
6 changed files with 38 additions and 8 deletions

View File

@ -135,6 +135,10 @@ npm run dev:h5
当前实现:**提交时登录 + guest 草稿恢复**。未登录用户可填写预约表单,提交时跳转登录页(带 `redirect` 回预约页并保留 `storeId`),登录成功后回到预约页并恢复 guest 草稿(草稿 key 不依赖 `userInfo.id`,避免 `userId: undefined`)。登录态用户继续按 `userId` 维度保存草稿。草稿有效期 7 天。 当前实现:**提交时登录 + guest 草稿恢复**。未登录用户可填写预约表单,提交时跳转登录页(带 `redirect` 回预约页并保留 `storeId`),登录成功后回到预约页并恢复 guest 草稿(草稿 key 不依赖 `userInfo.id`,避免 `userId: undefined`)。登录态用户继续按 `userId` 维度保存草稿。草稿有效期 7 天。
### 门店代客预约身份
老板/员工在首页新建预约时必须填写宠主手机号,可选填宠主称呼。后端按手机号关联或创建 customer并分别记录 `customerUserId``createdByUserId`;前端不得再把当前员工 `userId` 当作宠主提交。预约详情优先读取 `assignedStaffId`,兼容旧字段 `assignedUserId`
## 前端 RC 发布门禁 ## 前端 RC 发布门禁
发布前必须依次执行并全部通过: 发布前必须依次执行并全部通过:

View File

@ -83,7 +83,7 @@ export const getAppointmentList = (/* userId, storeId, */ options = {}) =>
// 预约详情 // 预约详情
export const getAppointmentDetail = (id) => get('/appointment/detail', { id }) export const getAppointmentDetail = (id) => get('/appointment/detail', { id })
// 创建预约userId/storeId 由后端从上下文派生;前端仍传 storeId 供 customer 选店场景,后端会按 role 决定是否采纳) // 创建预约登录人身份始终由后端派生customer 传选中门店,门店代客预约传 customerUserId 或 customerPhone
export const createAppointment = (data) => post('/appointment/create', data) export const createAppointment = (data) => post('/appointment/create', data)
/** 门店某日可预约半小时档(可约 / 已满 / 已过);公开接口 */ /** 门店某日可预约半小时档(可约 / 已满 / 已过);公开接口 */

View File

@ -42,9 +42,9 @@
<text class="label">备注</text> <text class="label">备注</text>
<text class="value remark">{{ detail.remark }}</text> <text class="value remark">{{ detail.remark }}</text>
</view> </view>
<view v-if="detail.assignedUserId" class="detail-row"> <view v-if="detail.assignedStaffId || detail.assignedUserId" class="detail-row">
<text class="label">服务技师</text> <text class="label">服务技师</text>
<text class="value">#{{ detail.assignedUserId }}</text> <text class="value">#{{ detail.assignedStaffId || detail.assignedUserId }}</text>
</view> </view>
<view class="detail-meta"> <view class="detail-meta">
<text v-if="detail.id">预约编号 {{ detail.id }}</text> <text v-if="detail.id">预约编号 {{ detail.id }}</text>

View File

@ -593,7 +593,7 @@ const confirmNewAppt = async () => {
uni.showToast({ title: '宠物档案保存失败,请重试', icon: 'none' }) uni.showToast({ title: '宠物档案保存失败,请重试', icon: 'none' })
return return
} }
const payload = { ...a, storeId, userId: userInfo.id } const payload = { ...a, storeId }
payload.petId = ensuredPet.id payload.petId = ensuredPet.id
const res = await createAppointment(payload) const res = await createAppointment(payload)
creatingAppt.value = false creatingAppt.value = false

View File

@ -199,6 +199,20 @@
<text class="popup-close" @click="showNewAppt = false"></text> <text class="popup-close" @click="showNewAppt = false"></text>
</view> </view>
<view class="popup-body"> <view class="popup-body">
<view class="c-field">
<text class="app-form-label">宠主手机号</text>
<input
v-model="newAppt.customerPhone"
class="app-form-input"
type="number"
maxlength="11"
placeholder="用于关联宠主与服务报告"
/>
</view>
<view class="c-field">
<text class="app-form-label">宠主称呼可选</text>
<input v-model="newAppt.customerName" class="app-form-input" maxlength="64" placeholder="例如:小白妈妈" />
</view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">宠物名字</text> <text class="app-form-label">宠物名字</text>
<input v-model="newAppt.petName" class="app-form-input" placeholder="请输入" /> <input v-model="newAppt.petName" class="app-form-input" placeholder="请输入" />
@ -387,7 +401,16 @@ const petTypes = [
{ label: '其他', value: '其他' } { label: '其他', value: '其他' }
] ]
const newAppt = ref({ petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' }) const emptyStaffAppointment = () => ({
customerPhone: '',
customerName: '',
petName: '',
petType: '',
serviceType: '',
appointmentTime: '',
remark: ''
})
const newAppt = ref(emptyStaffAppointment())
const appointmentTime = computed(() => { const appointmentTime = computed(() => {
const raw = newAppt.value.appointmentTime || '' const raw = newAppt.value.appointmentTime || ''
if (!raw.includes('T')) return '' if (!raw.includes('T')) return ''
@ -671,6 +694,10 @@ const goReport = (item) => {
const confirmNewAppt = async () => { const confirmNewAppt = async () => {
const a = newAppt.value const a = newAppt.value
if (!/^1[3-9]\d{9}$/.test(a.customerPhone || '')) {
uni.showToast({ title: '请输入正确的宠主手机号', icon: 'none' })
return
}
if (!a.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return } if (!a.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return }
if (!a.petType) { uni.showToast({ title: '请选择宠物类型', icon: 'none' }); return } if (!a.petType) { uni.showToast({ title: '请选择宠物类型', icon: 'none' }); return }
if (!a.serviceType) { uni.showToast({ title: '请选择服务类型', icon: 'none' }); return } if (!a.serviceType) { uni.showToast({ title: '请选择服务类型', icon: 'none' }); return }
@ -681,11 +708,11 @@ const confirmNewAppt = async () => {
return return
} }
creatingAppt.value = true creatingAppt.value = true
const res = await createAppointment({ ...a, storeId: storeInfo.id, userId: userInfo.id }) const res = await createAppointment({ ...a, storeId: storeInfo.id })
creatingAppt.value = false creatingAppt.value = false
if (res.code === 200) { if (res.code === 200) {
uni.showToast({ title: '预约成功', icon: 'success' }) uni.showToast({ title: '预约成功', icon: 'success' })
newAppt.value = { petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' } newAppt.value = emptyStaffAppointment()
showNewAppt.value = false showNewAppt.value = false
fetchAppointments() fetchAppointments()
} else { } else {

View File

@ -700,7 +700,6 @@ const submitReport = async () => {
const payload = { const payload = {
appointmentId: currentAppointmentId.value || null, appointmentId: currentAppointmentId.value || null,
userId: userInfo.id,
petName: report.value.petName, petName: report.value.petName,
serviceType: report.value.serviceType, serviceType: report.value.serviceType,
appointmentTime: report.value.appointmentTime || null, appointmentTime: report.value.appointmentTime || null,