diff --git a/YJYF/YJYF.vue b/YJYF/YJYF.vue
index e974082..e3bad6d 100644
--- a/YJYF/YJYF.vue
+++ b/YJYF/YJYF.vue
@@ -84,18 +84,18 @@
-
+
- {{ c.n }}
+ {{ c.name }}
@@ -226,6 +226,9 @@
+
diff --git a/pages/master/apply.vue b/pages/master/apply.vue
new file mode 100644
index 0000000..a7156d8
--- /dev/null
+++ b/pages/master/apply.vue
@@ -0,0 +1,137 @@
+
+
+
+ 师傅入驻
+ 实名认证 + 技能认证,审核通过即可接单
+
+
+
+ 当前状态:{{ statusText }}
+ 审核意见:{{ info.master.audit_msg }}
+
+
+
+ 真实姓名
+ 手机号
+ 身份证号
+ 服务城市
+
+ 选择服务类目(可多选)
+
+
+ {{ c.name }}
+
+
+
+ 证件与技能证书照片上传将在正式版接入 chooseMedia
+ {{ submitting ? '提交中...' : '提交审核' }}
+
+
+
+ 真实姓名{{ info.master.realname }}
+ 服务城市{{ info.master.city }}
+ 综合评分{{ info.master.score }}
+ 累计订单{{ info.master.order_count }}
+ 进入订单大厅 ›
+
+
+
+
+
+
+
+
diff --git a/pages/master/hall.vue b/pages/master/hall.vue
new file mode 100644
index 0000000..af77e8f
--- /dev/null
+++ b/pages/master/hall.vue
@@ -0,0 +1,119 @@
+
+
+
+
+ 订单大厅
+ {{ info ? '评分 ' + info.score + ' · 完成 ' + info.order_count + ' 单' : '加载中' }}
+
+
+ {{ info && info.work_status === 'off' ? '已暂停接单' : '接单中' }}
+
+
+
+
+
+
+ 急单
+ {{ o.service_name }}
+ ¥{{ o.pay_amount }}
+
+
+ {{ o.address }}
+ 楼层 {{ o.floor }} 层 · {{ Number(o.has_elevator) ? '有电梯' : '无电梯' }} · 预约 {{ o.appoint_text }}
+ 型号:{{ o.goods_model }}
+ 备注:{{ o.remark }}
+
+
+ {{ o.mobile }}
+ 立即抢单
+
+
+
+
+ 暂无待接订单,稍后刷新试试
+ 刷新大厅
+
+
+
+
+
+
diff --git a/pages/master/orders.vue b/pages/master/orders.vue
new file mode 100644
index 0000000..d216e2d
--- /dev/null
+++ b/pages/master/orders.vue
@@ -0,0 +1,130 @@
+
+
+
+
+ {{ t.name }}
+
+
+
+
+
+
+ {{ o.service_name }}
+ {{ o.status_text }}
+
+
+ {{ o.address }}
+ 预约 {{ o.appoint_text }} · 楼层 {{ o.floor }} 层 · {{ Number(o.has_elevator) ? '有电梯' : '无电梯' }}
+
+ 客户 {{ o.mobile }}
+ ¥{{ o.pay_amount }}
+
+
+
+ 标记已联系
+ 约定上门
+ 到达打卡
+ 开始服务
+ 完工提交
+ 打电话
+
+
+
+
+ 暂无订单
+
+
+
+
+
+
diff --git a/pages/order/create.vue b/pages/order/create.vue
new file mode 100644
index 0000000..65c8b8c
--- /dev/null
+++ b/pages/order/create.vue
@@ -0,0 +1,266 @@
+
+
+
+ {{ svc ? svc.name.slice(0, 2) : '' }}
+
+ {{ svc ? svc.name : '' }}
+ {{ svc ? svc.price_text : '' }}
+
+
+ {{ urgent ? '已加急 +20%' : '加急' }}
+
+
+
+
+ 服务地址
+
+
+ {{ address.contact }} {{ address.mobile }}
+ {{ address.province }}{{ address.city }}{{ address.district }}{{ address.address }}
+ 楼层 {{ address.floor }} · {{ Number(address.has_elevator) ? '有电梯' : '无电梯' }}
+
+ 更换
+
+
+
+ 联系人
+ 手机号
+ 详细地址
+
+ 楼层
+
+ -
+ {{ form.floor }}
+ +
+
+
+
+ 有无电梯
+
+ 有
+ 无
+
+
+ 保存地址
+
+
+
+
+ 预约与服务信息
+
+ 预约日期{{ date }}›
+
+
+ 预约时段{{ time }}›
+
+ 商品型号
+ 数量
+ -{{ form.goods_num }}+
+
+ 备注
+
+
+
+ 费用明细
+ 基础价¥{{ amount.base_amount }}
+ 楼层费¥{{ amount.floor_amount }}
+ 加急费¥{{ amount.urgent_amount }}
+ 应付合计¥{{ amount.pay_amount }}
+
+
+
+ 合计¥{{ amount ? amount.pay_amount : 0 }}
+ {{ submitting ? '提交中...' : '提交并支付' }}
+
+
+
+
+
+
+
diff --git a/pages/order/detail.vue b/pages/order/detail.vue
new file mode 100644
index 0000000..0ff0750
--- /dev/null
+++ b/pages/order/detail.vue
@@ -0,0 +1,168 @@
+
+
+
+ {{ order.status_text }}
+ {{ statusTip }}
+
+
+
+ 服务师傅
+
+ {{ (master.realname || '师').slice(0, 1) }}
+
+ {{ master.realname }}
+ 评分 {{ master.score }} · 完成 {{ master.order_count }} 单
+
+ {{ master.mobile }}
+
+
+
+
+ 服务信息
+ 服务项目{{ order.service_name }}
+ 订单号{{ order.order_no }}
+ 联系人{{ order.contact }} {{ order.mobile }}
+ 服务地址{{ order.province }}{{ order.city }}{{ order.district }}{{ order.address }}
+ 楼层{{ order.floor }} 层 · {{ Number(order.has_elevator) ? '有电梯' : '无电梯' }}
+ 预约时间{{ order.appoint_text }}
+ 商品型号{{ order.goods_model }}
+ 备注{{ order.remark }}
+ 加急是(+20%)
+
+
+
+ 费用明细
+ 基础价¥{{ order.base_amount }}
+ 楼层费¥{{ order.floor_amount }}
+ 加急费¥{{ order.urgent_amount }}
+ 实付¥{{ order.pay_amount }}
+
+
+
+ 订单轨迹
+
+
+
+ {{ l.status_text }}
+ {{ l.remark }}
+ {{ l.time_text }}
+
+
+
+
+
+ 现场取证
+
+
+ {{ m.stage === 'damage' ? '损坏申报' : '完工照片' }}
+
+
+
+
+
+ 取消订单
+ 去支付
+ 确认验收
+ 去评价
+ 返回首页
+
+
+
+
+
+
+
diff --git a/pages/order/list.vue b/pages/order/list.vue
new file mode 100644
index 0000000..63c1a58
--- /dev/null
+++ b/pages/order/list.vue
@@ -0,0 +1,130 @@
+
+
+
+
+ {{ t.name }}
+
+
+
+
+
+
+ {{ o.order_no }}
+ {{ o.status_text }}
+
+
+ {{ (o.service_name || '').slice(0, 2) }}
+
+ {{ o.service_name }}
+ {{ o.address }}
+ 预约 {{ o.appoint_text }}{{ Number(o.urgent) ? ' · 急单' : '' }}
+
+ ¥{{ o.pay_amount }}
+
+
+ 取消订单
+ 去支付
+ 确认验收
+ 去评价
+
+
+
+
+ 暂无订单
+ 仅展示前 {{ list.length }} 条
+
+
+
+
+
+
diff --git a/pages/order/review.vue b/pages/order/review.vue
new file mode 100644
index 0000000..4d463e8
--- /dev/null
+++ b/pages/order/review.vue
@@ -0,0 +1,99 @@
+
+
+
+ 给本次服务打分
+
+ ★
+
+ {{ scoreText }}
+
+
+
+ 服务标签
+
+
+ {{ t }}
+
+
+
+
+
+ 评价内容
+
+
+ 匿名评价
+ (anonymous = e.detail.value)" />
+
+
+
+ {{ submitting ? '提交中...' : '提交评价' }}
+
+
+
+
+
+
diff --git a/pages/service/detail.vue b/pages/service/detail.vue
new file mode 100644
index 0000000..890ed29
--- /dev/null
+++ b/pages/service/detail.vue
@@ -0,0 +1,103 @@
+
+
+
+ {{ detail.name.slice(0, 2) }}
+
+ {{ detail.name }}
+
+ {{ detail.price_type_text }}
+ {{ detail.cat_name }}
+
+ {{ detail.price_text }}
+
+
+
+
+ 费用说明
+ 基础价¥{{ detail.base_price }} / {{ detail.unit }}
+ 楼层费{{ detail.floor_fee }} 元/层({{ detail.free_floor }} 层内免)
+ 加急费基础价 20%
+ 无电梯超免楼层按楼层费计
+
+
+
+ 服务保障
+ 平台认证实名 + 技能审核
+ 验收付款客户验收后平台结算
+ 现场取证施工拍照留痕
+ {{ detail.description }}
+
+
+
+
+ {{ detail.price_text }}
+ 无隐藏费用
+
+ 立即下单
+
+
+
+
+
+
+
diff --git a/pages/service/list.vue b/pages/service/list.vue
new file mode 100644
index 0000000..2e44886
--- /dev/null
+++ b/pages/service/list.vue
@@ -0,0 +1,99 @@
+
+
+
+ {{ catName || '全部服务' }}
+ {{ list.length }} 项可预约
+
+
+
+
+ {{ s.name.slice(0, 2) }}
+
+ {{ s.name }}
+ {{ s.description || '专业师傅上门服务,验收满意再付款' }}
+
+ {{ s.price_text }}
+ 已售 {{ s.sales }}
+
+
+ 下单
+
+
+
+
+ 该类目暂无服务
+
+ 加载中...
+
+
+
+
+
+
diff --git a/server/application/api/controller/Master.php b/server/application/api/controller/Master.php
index 00ad1d0..f1b0e37 100644
--- a/server/application/api/controller/Master.php
+++ b/server/application/api/controller/Master.php
@@ -155,13 +155,17 @@ class Master extends Base
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(30, max(1, (int)$this->request->param('limit', 10)));
- $query = Db::name('order')->where('status', OrderStatus::PENDING_ACCEPT)->where('master_id', 0);
- if (!empty($catIds)) {
- $query->where('cat_id', 'in', $catIds);
- }
+ // TP5 中 count() 会重置查询条件,故用闭包重建
+ $build = function () use ($catIds) {
+ $q = Db::name('order')->where('status', OrderStatus::PENDING_ACCEPT)->where('master_id', 0);
+ if (!empty($catIds)) {
+ $q->where('cat_id', 'in', $catIds);
+ }
+ return $q;
+ };
- $total = $query->count();
- $list = $query->order('urgent desc, id desc')->page($page, $limit)->select();
+ $total = $build()->count();
+ $list = $build()->order('urgent desc, id desc')->page($page, $limit)->select();
foreach ($list as &$row) {
$row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门';
@@ -340,17 +344,21 @@ class Master extends Base
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(30, max(1, (int)$this->request->param('limit', 10)));
- $query = Db::name('order')->where('master_id', $master['id']);
- if ($status === 'active') {
- $query->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]);
- } elseif ($status === 'verify') {
- $query->where('status', OrderStatus::PENDING_VERIFY);
- } elseif ($status === 'completed') {
- $query->where('status', OrderStatus::COMPLETED);
- }
+ // TP5 中 count() 会重置查询条件,故用闭包重建
+ $build = function () use ($master, $status) {
+ $q = Db::name('order')->where('master_id', $master['id']);
+ if ($status === 'active') {
+ $q->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]);
+ } elseif ($status === 'verify') {
+ $q->where('status', OrderStatus::PENDING_VERIFY);
+ } elseif ($status === 'completed') {
+ $q->where('status', OrderStatus::COMPLETED);
+ }
+ return $q;
+ };
- $total = $query->count();
- $list = $query->order('id desc')->page($page, $limit)->select();
+ $total = $build()->count();
+ $list = $build()->order('id desc')->page($page, $limit)->select();
foreach ($list as &$row) {
$row['status_text'] = OrderStatus::label($row['status']);
$row['pay_amount'] = (float)$row['pay_amount'];
diff --git a/server/application/api/controller/Order.php b/server/application/api/controller/Order.php
index 0ef24e5..f2b19f7 100644
--- a/server/application/api/controller/Order.php
+++ b/server/application/api/controller/Order.php
@@ -160,19 +160,23 @@ class Order extends Base
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(50, max(1, (int)$this->request->param('limit', 10)));
- $query = Db::name('order')->where('user_id', $this->uid());
- if ($status === 'active') {
- $query->where('status', 'in', OrderStatus::activeValues());
- } elseif ($status === 'completed') {
- $query->where('status', OrderStatus::COMPLETED);
- } elseif ($status === 'cancelled') {
- $query->where('status', OrderStatus::CANCELLED);
- } elseif ($status !== 'all' && $status !== '') {
- $query->where('status', (int)$status);
- }
+ // 注意:TP5 中 count() 执行后查询条件会被重置,必须重新构建查询
+ $build = function () use ($status) {
+ $q = Db::name('order')->where('user_id', $this->uid());
+ if ($status === 'active') {
+ $q->where('status', 'in', OrderStatus::activeValues());
+ } elseif ($status === 'completed') {
+ $q->where('status', OrderStatus::COMPLETED);
+ } elseif ($status === 'cancelled') {
+ $q->where('status', OrderStatus::CANCELLED);
+ } elseif ($status !== 'all' && $status !== '') {
+ $q->where('status', (int)$status);
+ }
+ return $q;
+ };
- $total = $query->count();
- $list = $query->order('id desc')->page($page, $limit)->select();
+ $total = $build()->count();
+ $list = $build()->order('id desc')->page($page, $limit)->select();
foreach ($list as &$row) {
$row = $this->formatOrder($row);
}
diff --git a/tests/page_flow.js b/tests/page_flow.js
new file mode 100644
index 0000000..beb3d62
--- /dev/null
+++ b/tests/page_flow.js
@@ -0,0 +1,202 @@
+/**
+ * 逐页面全链路验证(从登录页开始)
+ * 按每个页面真实的接口调用顺序回放,验证:页面数据绑定是否拿得到、按钮状态是否正确
+ *
+ * 运行:node tests/page_flow.js
+ */
+const { execFileSync } = require('child_process');
+
+const BASE = process.env.API_BASE || 'https://yjyf.gzyakj.com.cn/api';
+let pass = 0, fail = 0;
+const failures = [];
+
+const ok = (m) => { pass++; console.log(' ✅ ' + m); };
+const bad = (m, why) => { fail++; failures.push(m + ' :: ' + why); console.log(' ❌ ' + m + ' → ' + why); };
+const chk = (c, m, why) => (c ? ok(m) : bad(m, why || '断言失败'));
+const page = (t) => console.log('\n▌' + t);
+
+async function api(path, opt) {
+ const o = opt || {};
+ const headers = {};
+ if (o.token) headers.token = o.token;
+ let body;
+ if (o.data) {
+ headers['content-type'] = 'application/x-www-form-urlencoded';
+ body = new URLSearchParams(o.data).toString();
+ }
+ const res = await fetch(BASE + path, { method: o.method || 'GET', headers, body });
+ const text = await res.text();
+ try { return JSON.parse(text); } catch (e) { throw new Error('非 JSON: ' + text.slice(0, 120)); }
+}
+const sql = (s) => execFileSync('ssh', ['-o', 'BatchMode=yes', 'root@8.137.181.63', `mysql -uyjyf -pyjyf yjyf -e "${s}"`], { encoding: 'utf8' }).trim();
+
+(async () => {
+ const stamp = String(Date.now()).slice(-8);
+ const uM = '139' + stamp, mM = '137' + stamp;
+ let uToken = '', mToken = '', orderId = 0, masterId = 0;
+
+ console.log('═══════════════════════════════════════════════════════');
+ console.log(' 优匠优服 · 从登录开始的逐页面全链路验证');
+ console.log(' 后端: ' + BASE);
+ console.log('═══════════════════════════════════════════════════════');
+
+ // ── 页面 1:pages/login/login ────────────────────────────────
+ page('页面1 登录页 pages/login/login');
+ let r = await api('/auth/login', { method: 'POST', data: { mobile: uM, code: '123456' } });
+ chk(r.code === 1 && r.data.token, '手机号+验证码登录 → 拿到 token', r.msg);
+ uToken = r.data.token;
+ chk(r.data.userinfo.id > 0 && r.data.userinfo.is_master === false, '登录页读取 userinfo 正常(is_master=false)');
+ r = await api('/auth/login', { method: 'POST', data: { mobile: uM, code: '999999' } });
+ chk(r.code !== 1, '错误验证码被拒 → toast 文案可展示', r.msg);
+
+ // ── 页面 2:YJYF/YJYF 首页 ─────────────────────────────────
+ page('页面2 首页 YJYF/YJYF');
+ r = await api('/home/index', { token: uToken });
+ chk(r.code === 1 && r.data.cats.length === 15, '首页 15 个类目渲染数据就绪', '实际 ' + (r.data.cats || []).length);
+ chk(r.data.cats.every(c => c.name && c.icon), '每个类目都有 name + icon(图标库可匹配)');
+ chk(typeof r.data.badge.coupon === 'number' && typeof r.data.badge.unread === 'number', '首页角标 badge 字段就绪');
+ const cat0 = r.data.cats[0];
+
+ // ── 页面 3:pages/service/list ─────────────────────────────
+ page('页面3 服务列表 pages/service/list');
+ r = await api('/home/services?cat_id=' + cat0.id);
+ chk(r.code === 1 && r.data.list.length > 0, '按类目取服务列表(cat_id=' + cat0.id + ')', '共 ' + (r.data.list || []).length + ' 项');
+ const svc = r.data.list.find(s => s.price_type === 'fixed') || r.data.list[0];
+ chk(svc.price_text && svc.name, '列表卡片字段完整(name / price_text / sales)');
+ r = await api('/home/services?keyword=' + encodeURIComponent('安装'));
+ chk(r.code === 1 && r.data.list.length > 0, '首页搜索框 → keyword 检索可用', '命中 ' + (r.data.list || []).length + ' 项');
+
+ // ── 页面 4:pages/service/detail ───────────────────────────
+ page('页面4 服务详情 pages/service/detail');
+ r = await api('/home/detail?id=' + svc.id);
+ const detail = r.data.detail;
+ chk(r.code === 1 && detail.name === svc.name, '详情页服务信息加载', detail.name);
+ chk(detail.floor_fee >= 0 && detail.free_floor >= 0, '费用说明区块字段就绪(floor_fee / free_floor)');
+
+ // ── 页面 5:pages/order/create ─────────────────────────────
+ page('页面5 下单页 pages/order/create');
+ r = await api('/address/index', { token: uToken });
+ chk(r.code === 1, '进入下单页先拉地址列表');
+ r = await api('/home/calc?service_id=' + svc.id + '&floor=5&has_elevator=0&urgent=1', { token: uToken });
+ const c = r.data;
+ chk(c.floor_amount === 20 && c.urgent_amount === 16 && c.pay_amount === 116,
+ '费用试算实时联动(楼层20 + 加急16 = 116)', JSON.stringify({ f: c.floor_amount, u: c.urgent_amount, p: c.pay_amount }));
+
+ r = await api('/address/save', {
+ method: 'POST', token: uToken,
+ data: { contact: '页面验证用户', mobile: uM, province: '贵州省', city: '遵义市', district: '汇川区', address: '大连路 88 号 2 栋 501', floor: 5, has_elevator: 0, is_default: 1, lng: '106.92', lat: '27.72' }
+ });
+ chk(r.code === 1 && r.data.id > 0, '下单页内联新增地址 → 保存成功', 'addr_id=' + r.data.id);
+ const addrId = r.data.id;
+
+ r = await api('/order/create', {
+ method: 'POST', token: uToken,
+ data: { service_id: svc.id, address_id: addrId, appoint_time: Math.floor(Date.now() / 1000) + 7200, urgent: 1, goods_model: '三人位沙发', goods_num: 1, remark: '带工具' }
+ });
+ chk(r.code === 1 && r.data.order_id > 0, '提交订单 → 返回 order_id', r.data && r.data.order_no);
+ orderId = r.data.order_id;
+
+ r = await api('/order/pay', { method: 'POST', token: uToken, data: { order_id: orderId, pay_type: 'mock' } });
+ chk(r.code === 1, '自动支付 → 订单进入待接单', r.msg);
+
+ // ── 页面 6:pages/order/detail(用户侧首次查看) ──────────
+ page('页面6 订单详情 pages/order/detail(用户侧)');
+ r = await api('/order/detail?id=' + orderId, { token: uToken });
+ chk(r.data.order.status_text === '待接单', '详情页状态头显示「待接单」', r.data.order.status_text);
+ chk(r.data.logs.length >= 2, '轨迹时间线有数据', r.data.logs.length + ' 条');
+ chk(r.data.order.can_pay === false, '已支付 → 「去支付」按钮隐藏');
+
+ // ── 页面 7:pages/master/apply(师傅入驻) ─────────────────
+ page('页面7 师傅入驻 pages/master/apply');
+ r = await api('/auth/login', { method: 'POST', data: { mobile: mM, code: '123456' } });
+ mToken = r.data.token;
+ r = await api('/master/info', { token: mToken });
+ chk(r.code === 1 && r.data.applied === false, '未入驻 → 入驻页展示表单(applied=false)');
+ r = await api('/master/apply', {
+ method: 'POST', token: mToken,
+ data: { realname: '页面验证师傅', mobile: mM, idcard_no: '520101199203031234', city: '遵义市', 'skills[0]': cat0.id }
+ });
+ chk(r.code === 1 && r.data.master_id > 0, '提交实名+技能 → 进入待审核', 'master_id=' + r.data.master_id);
+ masterId = r.data.master_id;
+ r = await api('/master/info', { token: mToken });
+ chk(r.data.master.verify_status === 'pending', '入驻页显示「待审核」状态');
+ r = await api('/master/hall', { token: mToken });
+ chk(r.code !== 1, '审核中进入大厅被拦截 → 页面提示正确', r.msg);
+
+ sql(`UPDATE fa_master SET verify_status='pass' WHERE id=${masterId}`);
+ ok('(模拟平台后台审核通过)');
+
+ // ── 页面 8:pages/master/hall(订单大厅) ──────────────────
+ page('页面8 订单大厅 pages/master/hall');
+ r = await api('/master/info', { token: mToken });
+ chk(r.data.master.work_status === 'on', '大厅顶部展示「接单中」开关');
+ r = await api('/master/hall', { token: mToken });
+ const hallOrder = (r.data.list || []).find(o => Number(o.id) === orderId);
+ chk(!!hallOrder, '大厅列表能刷出该订单', '共 ' + r.data.total + ' 单');
+ chk(hallOrder.mobile.includes('****'), '大厅里客户手机号脱敏展示(抢单前)', hallOrder.mobile);
+ chk(Number(hallOrder.urgent) === 1, '急单标签展示');
+
+ r = await api('/master/grab', { method: 'POST', token: mToken, data: { order_id: orderId } });
+ chk(r.code === 1, '点击「立即抢单」成功', r.msg);
+ chk(r.data.user_mobile === uM, '抢单成功后才下发完整客户手机号');
+
+ // ── 页面 9:pages/master/orders(接单管理 / 履约) ──────────
+ page('页面9 接单管理 pages/master/orders');
+ r = await api('/master/orders?status=active', { token: mToken });
+ let mo = (r.data.list || []).find(o => Number(o.id) === orderId);
+ chk(!!mo && Number(mo.status) === 3, '「待处理」列表显示已接单', mo && mo.status_text);
+
+ r = await api('/master/contact', { method: 'POST', token: mToken, data: { order_id: orderId } });
+ chk(r.code === 1, '按钮「标记已联系」');
+ r = await api('/master/schedule', { method: 'POST', token: mToken, data: { order_id: orderId, appoint_time: Math.floor(Date.now() / 1000) + 1800 } });
+ chk(r.code === 1, '按钮「约定上门」→ 待上门');
+ r = await api('/master/arrive', { method: 'POST', token: mToken, data: { order_id: orderId, lng: '106.92', lat: '27.72' } });
+ chk(r.code === 1, '按钮「到达打卡」→ 已到达');
+ r = await api('/master/start', { method: 'POST', token: mToken, data: { order_id: orderId } }).catch(() => ({}));
+ if (!r.code) { r = await api('/master/startJob', { method: 'POST', token: mToken, data: { order_id: orderId } }); }
+ chk(r.code === 1, '按钮「开始服务」→ 服务中', r.msg);
+ r = await api('/master/finish', { method: 'POST', token: mToken, data: { order_id: orderId, 'images[0]': 'https://yjyf.gzyakj.com.cn/uploads/s.jpg' } });
+ chk(r.code === 1 && /^\d{4}$/.test(r.data.verify_code), '按钮「完工提交」→ 返回 4 位验收码', r.data && r.data.verify_code);
+
+ // ── 页面 6b:订单详情(用户侧验收) ────────────────────────
+ page('页面6b 订单详情 pages/order/detail(待验收态)');
+ r = await api('/order/detail?id=' + orderId, { token: uToken });
+ chk(Number(r.data.order.status) === 7, '状态头显示「待验收」', r.data.order.status_text);
+ chk(r.data.order.can_verify === true, '「确认验收」按钮出现');
+ chk(r.data.media.length === 1, '现场取证图片区块有数据');
+
+ r = await api('/order/verify', { method: 'POST', token: uToken, data: { order_id: orderId } });
+ chk(r.code === 1, '点击「确认验收」成功', r.msg);
+ r = await api('/order/detail?id=' + orderId, { token: uToken });
+ chk(Number(r.data.order.status) === 8, '状态头变为「已完成」', r.data.order.status_text);
+ chk(r.data.order.can_review === true, '「去评价」按钮出现');
+
+ // ── 页面 10:pages/order/review ────────────────────────────
+ page('页面10 评价页 pages/order/review');
+ r = await api('/review/create', {
+ method: 'POST', token: uToken,
+ data: { order_id: orderId, score: 5, content: '师傅专业,安装到位', tags: '准时,专业', is_anonymous: 0 }
+ });
+ chk(r.code === 1, '提交 5 星评价成功', r.msg);
+ r = await api('/review/masterList?master_id=' + masterId);
+ chk(r.code === 1 && r.data.total === 1 && Number(r.data.avg_score) === 5, '评价列表与师傅评分已刷新', 'avg=' + r.data.avg_score);
+
+ // ── 页面 11:pages/order/list(列表态回看) ─────────────────
+ page('页面11 我的订单 pages/order/list');
+ r = await api('/order/index?status=all', { token: uToken });
+ chk(r.code === 1 && r.data.total >= 1, '「全部」Tab 有数据', r.data.total + ' 单');
+ r = await api('/order/index?status=completed', { token: uToken });
+ const doneStatuses = (r.data.list || []).map(o => o.status + '/' + o.status_text);
+ chk((r.data.list || []).length > 0 && r.data.list.every(o => Number(o.status) === 8),
+ '「已完成」Tab 筛选正确', '返回 ' + doneStatuses.join(', '));
+ const item = r.data.list.find(o => Number(o.id) === orderId);
+ chk(item.can_review === false, '已评价 → 列表「去评价」按钮消失');
+
+ // ── 汇总 ───────────────────────────────────────────────────
+ console.log('\n═══════════════════════════════════════════════════════');
+ console.log(` 结果:通过 ${pass} 项 / 失败 ${fail} 项`);
+ if (fail) { console.log(' 失败明细:'); failures.forEach(f => console.log(' - ' + f)); process.exitCode = 1; }
+ else console.log(' 🎉 从登录到评价的全链路逐页面验证通过');
+ console.log(` 订单 orderId=${orderId} 师傅 masterId=${masterId}`);
+ console.log('═══════════════════════════════════════════════════════');
+})().catch(e => { console.error('\n💥 中断: ' + e.message); process.exitCode = 1; });
diff --git a/utils/api.js b/utils/api.js
new file mode 100644
index 0000000..830c04a
--- /dev/null
+++ b/utils/api.js
@@ -0,0 +1,50 @@
+/**
+ * 接口清单(与后端 application/api/controller 一一对应)
+ */
+import { get, post } from './request';
+
+export default {
+ /* ---------- 鉴权 ---------- */
+ login: (data) => post('/auth/login', data, { silent: true }),
+ profile: () => get('/auth/profile'),
+
+ /* ---------- 首页 / 服务 ---------- */
+ home: () => get('/home/index', {}, { silent: true }),
+ cats: () => get('/home/cats'),
+ services: (data) => get('/home/services', data),
+ serviceDetail: (id) => get('/home/detail', { id: id }),
+ calc: (data) => get('/home/calc', data),
+
+ /* ---------- 地址 ---------- */
+ addressList: () => get('/address/index'),
+ addressDetail: (id) => get('/address/detail', { id: id }),
+ addressSave: (data) => post('/address/save', data),
+ addressDelete: (id) => post('/address/delete', { id: id }),
+ addressDefault: (id) => post('/address/setDefault', { id: id }),
+
+ /* ---------- 订单(用户端) ---------- */
+ orderCreate: (data) => post('/order/create', data),
+ orderPay: (data) => post('/order/pay', data),
+ orderCancel: (data) => post('/order/cancel', data),
+ orderVerify: (data) => post('/order/verify', data),
+ orderList: (data) => get('/order/index', data),
+ orderDetail: (id) => get('/order/detail', { id: id }),
+
+ /* ---------- 师傅端 ---------- */
+ masterApply: (data) => post('/master/apply', data),
+ masterInfo: () => get('/master/info'),
+ masterHall: (data) => get('/master/hall', data),
+ masterGrab: (data) => post('/master/grab', data),
+ masterOrders: (data) => get('/master/orders', data),
+ masterContact: (data) => post('/master/contact', data),
+ masterSchedule: (data) => post('/master/schedule', data),
+ masterArrive: (data) => post('/master/arrive', data),
+ masterStart: (data) => post('/master/startJob', data),
+ masterFinish: (data) => post('/master/finish', data),
+ masterIncome: () => get('/master/income'),
+ masterToggle: (data) => post('/master/toggleWork', data),
+
+ /* ---------- 评价 ---------- */
+ reviewCreate: (data) => post('/review/create', data),
+ reviewList: (masterId) => get('/review/masterList', { master_id: masterId })
+};
diff --git a/utils/request.js b/utils/request.js
new file mode 100644
index 0000000..d4ea593
--- /dev/null
+++ b/utils/request.js
@@ -0,0 +1,107 @@
+/**
+ * 统一请求层
+ * - 自动携带 token(header: token)
+ * - 统一响应契约:code === 1 成功;其余为失败(0 通用 / 401 未登录 / 403 无权限 / 2001~2003 业务语义)
+ * - 失败统一 toast,未登录自动清理并引导登录
+ */
+const BASE_URL = 'https://yjyf.gzyakj.com.cn/api';
+
+const TOKEN_KEY = 'yjyf_token';
+const USER_KEY = 'yjyf_user';
+
+export function getToken() {
+ try {
+ return uni.getStorageSync(TOKEN_KEY) || '';
+ } catch (e) {
+ return '';
+ }
+}
+
+export function setToken(token) {
+ uni.setStorageSync(TOKEN_KEY, token || '');
+}
+
+export function getUser() {
+ try {
+ return uni.getStorageSync(USER_KEY) || null;
+ } catch (e) {
+ return null;
+ }
+}
+
+export function setUser(user) {
+ uni.setStorageSync(USER_KEY, user || null);
+}
+
+export function clearAuth() {
+ uni.removeStorageSync(TOKEN_KEY);
+ uni.removeStorageSync(USER_KEY);
+}
+
+export function isLogin() {
+ return !!getToken();
+}
+
+export function toLogin() {
+ clearAuth();
+ uni.navigateTo({ url: '/pages/login/login' });
+}
+
+/**
+ * @param {String} path 接口路径,如 /order/index
+ * @param {Object} options { method, data, auth, silent }
+ */
+export function request(path, options) {
+ const opts = options || {};
+ const method = opts.method || 'GET';
+ const data = opts.data || {};
+ const silent = !!opts.silent;
+
+ return new Promise((resolve, reject) => {
+ const header = { 'content-type': 'application/x-www-form-urlencoded' };
+ const token = getToken();
+ if (token) {
+ header.token = token;
+ }
+
+ uni.request({
+ url: BASE_URL + path,
+ method: method,
+ data: data,
+ header: header,
+ timeout: 15000,
+ success: (res) => {
+ const body = res.data || {};
+ if (body.code === 1) {
+ resolve(body.data === undefined || body.data === null ? {} : body.data);
+ return;
+ }
+ const msg = body.msg || ('请求失败(' + res.statusCode + ')');
+ if (body.code === 401) {
+ clearAuth();
+ if (!silent) {
+ uni.showToast({ title: '登录已失效,请重新登录', icon: 'none' });
+ setTimeout(() => toLogin(), 800);
+ }
+ } else if (!silent) {
+ uni.showToast({ title: msg, icon: 'none' });
+ }
+ reject(Object.assign(new Error(msg), { code: body.code, data: body.data }));
+ },
+ fail: () => {
+ if (!silent) {
+ uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+ }
+ reject(new Error('网络异常'));
+ }
+ });
+ });
+}
+
+export function get(path, data, options) {
+ return request(path, Object.assign({ method: 'GET', data: data }, options || {}));
+}
+
+export function post(path, data, options) {
+ return request(path, Object.assign({ method: 'POST', data: data }, options || {}));
+}