diff --git a/pwuniapp/pages/expert/detail.vue b/pwuniapp/pages/expert/detail.vue
index 0b17c5e..a4cdd1d 100644
--- a/pwuniapp/pages/expert/detail.vue
+++ b/pwuniapp/pages/expert/detail.vue
@@ -74,6 +74,12 @@
服务项目
+
+ 该大神暂未配置服务项目,可直接发起基础陪伴预约
+
{
photos: snapPhotos,
services: buildDefaultServices(snapGame, snapPrice)
};
- selectedServiceId.value = expert.value.services[0].id;
+ if (expert.value.services && expert.value.services.length > 0) {
+ selectedServiceId.value = expert.value.services[0].id;
+ }
}
try {
@@ -897,6 +905,20 @@ const submitReportTicket = async () => {
flex-direction: column;
gap: 16rpx;
+ .service-empty {
+ padding: 28rpx 24rpx;
+ border-radius: 16rpx;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px dashed rgba(255, 255, 255, 0.12);
+ text-align: center;
+
+ .service-empty-text {
+ font-size: 24rpx;
+ color: #94A3B8;
+ line-height: 1.6;
+ }
+ }
+
.service-item {
display: flex;
justify-content: space-between;
diff --git a/pwuniapp/pages/order/create.vue b/pwuniapp/pages/order/create.vue
index 4afe7d6..7c1158e 100644
--- a/pwuniapp/pages/order/create.vue
+++ b/pwuniapp/pages/order/create.vue
@@ -9,10 +9,10 @@
{{ expert.nickname }}
{{ expert.game }}
- {{ selectedService.title }}
+ {{ serviceView.title }}
- ¥{{ selectedService.price }}/{{ selectedService.unit }}
+ ¥{{ serviceView.price }}/{{ serviceView.unit }}
@@ -22,11 +22,11 @@
购买时长
- 以{{ selectedService.unit }}为计费单位
+ 以{{ serviceView.unit }}为计费单位
- {{ hours }} {{ selectedService.unit }}
+ {{ hours }} {{ serviceView.unit }}
@@ -314,6 +314,13 @@ const expert = ref({
services: []
});
const selectedService = ref(null);
+// 模板安全视图:即便服务包尚未加载完成,也不会出现 null.title 崩溃
+const serviceView = computed(() => selectedService.value || {
+ id: 'base',
+ title: '同城陪伴基础服务',
+ price: Number(expert.value && expert.value.price) || 35,
+ unit: '小时'
+});
const hours = ref(2);
const timeType = ref('NOW');
const discount = ref(0);
@@ -340,21 +347,43 @@ onLoad(async (query) => {
const liveExpert = await getCompanionDetail(targetId);
if (liveExpert) {
expert.value = liveExpert;
- if (query && query.serviceId && liveExpert.services) {
- const s = liveExpert.services.find(srv => String(srv.id) === String(query.serviceId));
+ const services = Array.isArray(liveExpert.services) ? liveExpert.services : [];
+ if (query && query.serviceId) {
+ const s = services.find(srv => String(srv.id) === String(query.serviceId));
if (s) {
selectedService.value = s;
- } else if (liveExpert.services.length > 0) {
- selectedService.value = liveExpert.services[0];
}
- } else if (liveExpert.services && liveExpert.services.length > 0) {
- selectedService.value = liveExpert.services[0];
+ }
+ if (!selectedService.value && services.length > 0) {
+ selectedService.value = services[0];
+ }
+ // 兜底:大神尚未配置服务包时,按其基础时薪提供"基础陪伴服务",保证仍可下单
+ // (价格仍以服务端大神档案价为准,createOrder 会二次校验)
+ if (!selectedService.value) {
+ selectedService.value = {
+ id: 'base',
+ title: `${liveExpert.game || '同城陪伴'}基础陪伴服务`,
+ price: Number(liveExpert.price) || 35,
+ unit: '小时',
+ desc: '大神暂未配置服务包,按基础时薪计费'
+ };
}
}
} catch (err) {
console.warn('[OrderCreate] 获取线上大神详情异常:', err);
}
+ // 极端兜底:详情获取失败时也要保证不会因空服务崩溃
+ if (!selectedService.value) {
+ selectedService.value = {
+ id: 'base',
+ title: '同城陪伴基础服务',
+ price: Number(expert.value.price) || 35,
+ unit: '小时',
+ desc: ''
+ };
+ }
+
// 加载后台真实可用优惠券
await loadLiveCoupons();
});
diff --git a/server/tests/null-service-guard.spec.ts b/server/tests/null-service-guard.spec.ts
new file mode 100644
index 0000000..fc29aa5
--- /dev/null
+++ b/server/tests/null-service-guard.spec.ts
@@ -0,0 +1,35 @@
+// 已读取AGENTS.md
+import * as fs from 'fs';
+import * as path from 'path';
+
+/**
+ * 第八十二批:服务包为空时的空指针防护。
+ * 背景:移除"系统自动生成演示服务包"后,大神可能没有任何服务包,
+ * 下单页模板直接取 selectedService.title 会崩(Cannot read properties of null)。
+ */
+describe('第八十二批服务包空值防护闸门', () => {
+ const root = path.resolve(__dirname, '../../');
+ const createPage = fs.readFileSync(path.join(root, 'pwuniapp/pages/order/create.vue'), 'utf-8');
+ const detailPage = fs.readFileSync(path.join(root, 'pwuniapp/pages/expert/detail.vue'), 'utf-8');
+
+ it('下单页提供 serviceView 安全视图,模板不再直接解引用 selectedService', () => {
+ expect(createPage).toContain('const serviceView = computed(');
+ expect(createPage).toContain('{{ serviceView.title }}');
+ expect(createPage).toContain('{{ serviceView.price }}');
+ expect(createPage).toContain('{{ serviceView.unit }}');
+ expect(createPage).not.toContain('selectedService.title');
+ expect(createPage).not.toContain('selectedService.price');
+ expect(createPage).not.toContain('selectedService.unit');
+ });
+
+ it('下单页在无服务包时按大神基础时薪兜底,保证仍可下单', () => {
+ expect(createPage).toContain("id: 'base'");
+ expect(createPage).toContain('基础陪伴服务');
+ expect(createPage).toContain('Array.isArray(liveExpert.services) ? liveExpert.services : []');
+ });
+
+ it('详情页取 services[0] 前先判空,并提供空态提示', () => {
+ expect(detailPage).toContain('expert.value.services && expert.value.services.length > 0');
+ expect(detailPage).toContain('该大神暂未配置服务项目');
+ });
+});