文件
yjyf/server/application/api/controller/Order.php
linda 4e0c86ca06 feat(sync): 后端功能模块与前端模块数据同源 + 后台13个业务模块可管
前端不再写死运营数据:
- 首页 Banner/保障条/教程卡 改由 /api/home/index 下发(fa_banner + fa_config),
  数据缺失或停用自动回落内置兜底文案
- 订单状态提示改由后端 OrderStatus::tip() 下发 status_tip,前后端一套口径

后台可管(FastAdmin 生成器):
- 新增 13 个业务后台模块:运营位/订单/师傅/商家/优惠券/评价/消息/服务/服务类目/申诉/开票/提现/结算单
  (controller + model + view + fa_auth_rule 菜单 78 条)

修复:
- nginx /admin/* 改为 301 到 /admin.php/*,否则后台菜单点开全部 404

验证:tests/phase5_data_sync.js 新增 16 项联动断言;全量回归 202 项断言 0 失败
2026-10-28 17:06:55 +08:00

362 行
14 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use app\common\library\OrderStatus;
use think\Db;
/**
* 用户端订单
*/
class Order extends Base
{
/**
* 状态字典(供前端渲染筛选 Tab)
*/
public function statusList()
{
$this->success('', ['list' => OrderStatus::all()]);
}
/**
* 下单(创建待支付订单)
* @ApiMethod (POST)
*/
public function create()
{
$serviceId = (int)$this->request->post('service_id');
$addressId = (int)$this->request->post('address_id');
$appointTime = (int)$this->request->post('appoint_time', 0);
$urgent = (int)$this->request->post('urgent', 0);
$couponId = (int)$this->request->post('coupon_id', 0);
$goodsModel = trim((string)$this->request->post('goods_model'));
$goodsNum = max(1, (int)$this->request->post('goods_num', 1));
$remark = trim((string)$this->request->post('remark'));
$goodsImages = $this->request->post('goods_images', '');
$service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find();
if (!$service) {
$this->error('服务不存在或已下架');
}
$address = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find();
if (!$address) {
$this->error('请选择服务地址');
}
if ($appointTime > 0 && $appointTime < time()) {
$this->error('预约时间不能早于当前时间');
}
// 金额统一由 Home::calcAmount 计算,保证与试算口径一致
$amount = Home::calcAmount($service, [
'floor' => (int)$address['floor'],
'has_elevator' => (int)$address['has_elevator'],
'urgent' => $urgent,
], $couponId, $this->uid());
$orderNo = $this->makeOrderNo();
$verifyCode = (string)mt_rand(1000, 9999);
$id = Db::name('order')->insertGetId([
'order_no' => $orderNo,
'user_id' => $this->uid(),
'master_id' => 0,
'cat_id' => (int)$service['cat_id'],
'service_id' => (int)$service['id'],
'service_name' => $service['name'],
'service_image' => $service['image'],
'price_type' => $service['price_type'],
'goods_images' => is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages,
'goods_model' => mb_substr($goodsModel, 0, 100),
'goods_num' => $goodsNum,
'floor' => (int)$address['floor'],
'has_elevator' => (int)$address['has_elevator'],
'urgent' => $urgent ? 1 : 0,
'remark' => mb_substr($remark, 0, 500),
'contact' => $address['contact'],
'mobile' => $address['mobile'],
'province' => $address['province'],
'city' => $address['city'],
'district' => $address['district'],
'address' => $address['address'],
'lng' => $address['lng'],
'lat' => $address['lat'],
'appoint_time' => $appointTime ?: null,
'base_amount' => $amount['base_amount'],
'floor_amount' => $amount['floor_amount'],
'urgent_amount' => $amount['urgent_amount'],
'discount_amount' => $amount['discount_amount'],
'total_amount' => $amount['total_amount'],
'pay_amount' => $amount['pay_amount'],
'status' => OrderStatus::PENDING_PAY,
'pay_status' => 0,
'coupon_id' => $amount['coupon'] ? (int)$amount['coupon']['id'] : 0,
'verify_code' => $verifyCode,
'createtime' => time(),
'updatetime' => time(),
]);
$this->addLog($id, OrderStatus::PENDING_PAY, '用户下单', 'user', $this->uid());
$this->success('下单成功', [
'order_id' => (int)$id,
'order_no' => $orderNo,
'amount' => $amount,
]);
}
/**
* 支付(联调模拟支付,pay_type=mock;上线换微信支付回调)
* @ApiMethod (POST)
*/
public function pay()
{
$orderId = (int)$this->request->post('order_id');
$payType = $this->request->post('pay_type', 'mock');
$order = $this->ownOrder($orderId, 'user');
if ((int)$order['pay_status'] === 1) {
$this->error('订单已支付,请勿重复支付');
}
if ((int)$order['status'] !== OrderStatus::PENDING_PAY) {
$this->error('订单状态不允许支付');
}
Db::startTrans();
try {
Db::name('order')->where('id', $orderId)->update([
'pay_status' => 1,
'pay_type' => $payType,
'pay_time' => time(),
'transaction_id' => 'MOCK' . date('YmdHis') . mt_rand(1000, 9999),
'updatetime' => time(),
]);
$this->transit($order, OrderStatus::PENDING_ACCEPT, '支付成功,订单已进入接单大厅', [], 'user', $this->uid());
$this->addLog($orderId, OrderStatus::PENDING_ACCEPT, '订单进入师傅接单大厅', 'system', 0);
// 核销优惠券
if ((int)$order['coupon_id'] > 0) {
Db::name('user_coupon')->where('id', (int)$order['coupon_id'])->update([
'status' => 'used',
'order_id' => $orderId,
'use_time' => time(),
]);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('支付失败:' . $e->getMessage());
}
$this->success('支付成功');
}
/**
* 我的订单列表
*/
public function index()
{
$status = $this->request->param('status', 'all');
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(50, max(1, (int)$this->request->param('limit', 10)));
// 注意: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 = $build()->count();
$list = $build()->order('id desc')->page($page, $limit)->select();
foreach ($list as &$row) {
$row = $this->formatOrder($row);
}
unset($row);
$this->success('', ['list' => $list, 'total' => (int)$total, 'page' => $page]);
}
/**
* 订单详情(含状态轨迹与师傅信息)
*/
public function detail()
{
$orderId = (int)$this->request->param('id');
$order = $this->ownOrder($orderId, 'user');
$logs = Db::name('order_log')->where('order_id', $orderId)->order('id asc')->select();
foreach ($logs as &$log) {
$log['status_text'] = OrderStatus::label($log['status']);
$log['time_text'] = date('m-d H:i', (int)$log['createtime']);
}
unset($log);
$master = null;
if ((int)$order['master_id'] > 0) {
$m = Db::name('master')->where('id', $order['master_id'])->find();
if ($m) {
$master = [
'id' => (int)$m['id'],
'realname' => $m['realname'],
'mobile' => substr_replace($m['mobile'], '****', 3, 4),
'score' => (float)$m['score'],
'order_count' => (int)$m['order_count'],
'level' => (int)$m['level'],
];
}
}
$media = Db::name('order_media')->where('order_id', $orderId)->order('id asc')->select();
$this->success('', [
'order' => $this->formatOrder($order),
'master' => $master,
'logs' => $logs,
'media' => $media,
]);
}
/**
* 取消订单(已支付则模拟退款)
* @ApiMethod (POST)
*/
public function cancel()
{
$orderId = (int)$this->request->post('order_id');
$reason = trim((string)$this->request->post('reason', '用户取消'));
$order = $this->ownOrder($orderId, 'user');
if (!OrderStatus::canCancelByUser($order['status'])) {
$this->error('当前状态(' . OrderStatus::label($order['status']) . ')不可取消,请联系客服');
}
Db::startTrans();
try {
$extra = ['cancel_reason' => mb_substr($reason, 0, 255)];
if ((int)$order['pay_status'] === 1) {
$extra['pay_status'] = 2;
$extra['refund_amount'] = $order['pay_amount'];
}
$this->transit($order, OrderStatus::CANCELLED, '用户取消:' . $reason, $extra, 'user', $this->uid());
if ((int)$order['coupon_id'] > 0) {
Db::name('user_coupon')->where('id', (int)$order['coupon_id'])->update([
'status' => 'unused', 'order_id' => 0, 'use_time' => null,
]);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('取消失败:' . $e->getMessage());
}
$this->success('订单已取消');
}
/**
* 客户验收(验收码可选校验)→ 生成结算并给师傅入账
* @ApiMethod (POST)
*/
public function verify()
{
$orderId = (int)$this->request->post('order_id');
$code = trim((string)$this->request->post('verify_code', ''));
$order = $this->ownOrder($orderId, 'user');
if ((int)$order['status'] !== OrderStatus::PENDING_VERIFY) {
$this->error('当前状态(' . OrderStatus::label($order['status']) . ')不可验收');
}
if ($code !== '' && $code !== (string)$order['verify_code']) {
$this->error('验收码不正确');
}
$masterId = (int)$order['master_id'];
$payAmount = (float)$order['pay_amount'];
$rate = 10.00;
$commission = round($payAmount * $rate / 100, 2);
$settleAmount = round($payAmount - $commission, 2);
Db::startTrans();
try {
$this->transit($order, OrderStatus::COMPLETED, '客户验收通过,订单完成', [
'verify_time' => time(),
'settle_status' => 1,
'settle_amount' => $settleAmount,
], 'user', $this->uid());
Db::name('settle')->insert([
'order_id' => $orderId,
'master_id' => $masterId,
'order_amount' => $payAmount,
'commission_rate' => $rate,
'commission' => $commission,
'settle_amount' => $settleAmount,
'status' => 'settled',
'settle_time' => time(),
'createtime' => time(),
]);
if ($masterId > 0) {
$master = Db::name('master')->where('id', $masterId)->lock(true)->find();
$newMoney = round((float)$master['money'] + $settleAmount, 2);
Db::name('master')->where('id', $masterId)->update([
'money' => $newMoney,
'order_count' => (int)$master['order_count'] + 1,
'updatetime' => time(),
]);
Db::name('wallet_log')->insert([
'user_type' => 'master',
'user_id' => $masterId,
'type' => 'settle',
'amount' => $settleAmount,
'balance' => $newMoney,
'order_id' => $orderId,
'remark' => '订单结算入账',
'createtime' => time(),
]);
$this->notify('master', (int)$master['user_id'], '订单结算入账', '订单 ' . $order['order_no'] . ' 已验收,收入 ¥' . $settleAmount, 'order', $orderId);
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('验收失败:' . $e->getMessage());
}
$this->success('验收成功,订单已完成');
}
/**
* 订单输出格式化
*/
protected function formatOrder($row)
{
$status = (int)$row['status'];
$appoint = (int)$row['appoint_time'];
return array_merge($row, [
'id' => (int)$row['id'],
'status' => $status,
'status_text' => OrderStatus::label($status),
'status_tip' => OrderStatus::tip($status),
'can_cancel' => OrderStatus::canCancelByUser($status),
'can_verify' => $status === OrderStatus::PENDING_VERIFY,
'can_review' => $status === OrderStatus::COMPLETED && (int)$row['is_review'] === 0,
'can_pay' => $status === OrderStatus::PENDING_PAY,
'pay_amount' => (float)$row['pay_amount'],
'total_amount' => (float)$row['total_amount'],
'appoint_text' => $appoint ? date('m-d H:i', $appoint) : '尽快上门',
'create_text' => date('Y-m-d H:i', (int)$row['createtime']),
'urgent' => (int)$row['urgent'],
]);
}
}