trim((string)$this->request->post('name')), 'license_no' => trim((string)$this->request->post('license_no')), 'license_image' => (string)$this->request->post('license_image'), 'legal_person' => trim((string)$this->request->post('legal_person')), 'contact' => trim((string)$this->request->post('contact')), 'mobile' => trim((string)$this->request->post('mobile')), 'province' => trim((string)$this->request->post('province')), 'city' => trim((string)$this->request->post('city')), 'district' => trim((string)$this->request->post('district')), 'address' => trim((string)$this->request->post('address')), 'invoice_title' => trim((string)$this->request->post('invoice_title')), 'invoice_tax_no' => trim((string)$this->request->post('invoice_tax_no')), 'invoice_bank' => trim((string)$this->request->post('invoice_bank')), 'invoice_account' => trim((string)$this->request->post('invoice_account')), 'settle_type' => $this->request->post('settle_type', 'prepay'), ]; if ($data['name'] === '' || $data['license_no'] === '' || $data['contact'] === '' || $data['mobile'] === '') { $this->error('请填写企业名称、统一社会信用代码、联系人和电话'); } if (!preg_match('/^1\d{10}$/', $data['mobile'])) { $this->error('联系电话格式不正确'); } if (!in_array($data['settle_type'], ['prepay', 'monthly'], true)) { $data['settle_type'] = 'prepay'; } // 已被商家绑定的关联账号,企业资料由主账号维护 if (Db::name('merchant_member')->where('user_id', $this->uid())->find()) { $this->error('当前账号已绑定商家,企业资料请由主账号维护'); } $exist = Db::name('merchant')->where('user_id', $this->uid())->find(); if ($exist && $exist['verify_status'] === 'pass') { $this->error('您已是认证商家,无需重复入驻'); } $save = array_merge($data, [ 'verify_status' => 'pending', 'audit_msg' => '', 'updatetime' => time(), ]); if ($exist) { Db::name('merchant')->where('id', $exist['id'])->update($save); $id = (int)$exist['id']; } else { $save['user_id'] = $this->uid(); $save['createtime'] = time(); $id = Db::name('merchant')->insertGetId($save); } $this->success('提交成功,请等待审核', ['merchant_id' => $id]); } /** * 商家档案与经营数据 */ public function info() { $merchant = $this->merchantInfo(); if (!$merchant) { $this->success('未入驻', ['applied' => false]); return; } $uids = $this->merchantUserIds($merchant); $isOwner = (int)$merchant['user_id'] === $this->uid(); $orderCount = (int)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->where('status', '<>', OrderStatus::CANCELLED)->count(); $quota = InvoiceService::getQuotaSummary($this->uid()); $totalAmount = (float)$quota['paid_amount']; $invoiced = (float)$quota['used_amount']; $this->success('', [ 'applied' => true, 'is_owner' => $isOwner, 'role' => $isOwner ? 'owner' : 'member', 'merchant' => [ 'id' => (int)$merchant['id'], 'name' => $merchant['name'], 'license_no' => $merchant['license_no'], 'legal_person' => $merchant['legal_person'], 'contact' => $merchant['contact'], 'mobile' => $merchant['mobile'], 'province' => (string)$merchant['province'], 'city' => (string)$merchant['city'], 'district' => (string)$merchant['district'], 'detail_address' => (string)$merchant['address'], 'address' => $merchant['province'] . $merchant['city'] . $merchant['district'] . $merchant['address'], 'invoice_title' => $merchant['invoice_title'], 'invoice_tax_no' => $merchant['invoice_tax_no'], 'invoice_bank' => $merchant['invoice_bank'], 'invoice_account' => $merchant['invoice_account'], 'settle_type' => $merchant['settle_type'], 'verify_status' => $merchant['verify_status'], 'audit_msg' => $merchant['audit_msg'], 'role' => $isOwner ? 'owner' : 'member', 'is_owner' => $isOwner, 'owner_user_id' => (int)$merchant['user_id'], 'member_count' => max(0, count($uids) - 1), 'order_count' => $orderCount, 'total_amount' => round($totalAmount, 2), 'invoiced_amount' => round($invoiced, 2), 'issued_amount' => round((float)$quota['issued_amount'], 2), 'wait_invoice_amount' => round((float)$quota['wait_amount'], 2), 'invoiceable' => round((float)$quota['available'], 2), ], ]); } /** * 维护资料(联系人 / 地址 / 开票信息) * @ApiMethod (POST) */ public function update() { $merchant = $this->requireMerchant(); $this->requireMerchantOwner($merchant); $fields = ['contact', 'mobile', 'province', 'city', 'district', 'address', 'invoice_title', 'invoice_tax_no', 'invoice_bank', 'invoice_account']; $data = ['updatetime' => time()]; foreach ($fields as $f) { $v = $this->request->post($f, null); if ($v !== null) { $data[$f] = trim((string)$v); } } if (isset($data['mobile']) && $data['mobile'] !== '' && !preg_match('/^1\d{10}$/', $data['mobile'])) { $this->error('联系电话格式不正确'); } Db::name('merchant')->where('id', $merchant['id'])->update($data); // 若维护了商家开票资料,同步刷新关联的商家默认发票抬头 if (isset($data['invoice_title']) || isset($data['invoice_tax_no']) || isset($data['invoice_bank']) || isset($data['invoice_account'])) { $latest = Db::name('merchant')->where('id', $merchant['id'])->find(); if ($latest) { $syncFields = ['updatetime' => time()]; if (!empty($latest['invoice_title'])) $syncFields['title'] = $latest['invoice_title']; if (!empty($latest['invoice_tax_no'])) $syncFields['tax_no'] = strtoupper($latest['invoice_tax_no']); if (isset($data['invoice_bank'])) $syncFields['bank_name'] = $latest['invoice_bank']; if (isset($data['invoice_account'])) $syncFields['bank_account'] = $latest['invoice_account']; Db::name('invoice_title') ->where('merchant_id', (int)$merchant['id']) ->where('title_type', 'enterprise') ->update($syncFields); } } $this->success('已保存'); } /** * 账单中心:按月汇总 + 近期订单 */ public function bills() { $merchant = $this->requireMerchant(); $uids = $this->merchantUserIds($merchant); $months = Db::name('order') ->field("FROM_UNIXTIME(createtime,'%Y-%m') as month, count(*) as cnt, sum(pay_amount) as amount") ->where('user_id', 'in', $uids)->where('pay_status', 1) ->group('month')->order('month desc')->limit(12)->select(); foreach ($months as &$m) { $m['amount'] = round((float)$m['amount'], 2); $m['cnt'] = (int)$m['cnt']; } unset($m); $orders = Db::name('order')->field('id,order_no,service_name,pay_amount,status,createtime') ->where('user_id', 'in', $uids)->order('id desc')->limit(20)->select(); foreach ($orders as &$o) { $o['pay_amount'] = (float)$o['pay_amount']; $o['status_text'] = \app\common\library\OrderStatus::label($o['status']); $o['time_text'] = date('Y-m-d H:i', (int)$o['createtime']); } unset($o); $this->success('', ['months' => $months, 'orders' => $orders]); } /** * 企业订单:主账号 + 关联账号的全部订单 */ public function orders() { $merchant = $this->requireMerchant(); $uids = $this->merchantUserIds($merchant); $status = $this->request->param('status', 'all'); $keyword = trim((string)$this->request->param('keyword', '')); $memberUid = (int)$this->request->param('member_uid', 0); $month = trim((string)$this->request->param('month', '')); $page = max(1, (int)$this->request->param('page', 1)); $limit = min(50, max(1, (int)$this->request->param('limit', 20))); $targetUids = ($memberUid > 0 && in_array($memberUid, $uids, true)) ? [$memberUid] : $uids; $build = function () use ($targetUids, $status, $keyword, $month) { $q = Db::name('order')->where('user_id', 'in', $targetUids); if ($month !== '' && preg_match('/^\d{4}-\d{2}$/', $month)) { $startTs = strtotime($month . '-01 00:00:00'); $endTs = strtotime('+1 month', $startTs) - 1; if ($startTs > 0 && $endTs > 0) { $q->where('createtime', 'between', [$startTs, $endTs]); } } if ($keyword !== '') { $q->where(function ($sub) use ($keyword) { $sub->where('contact', 'like', "%{$keyword}%") ->whereOr('mobile', 'like', "%{$keyword}%") ->whereOr('order_no', 'like', "%{$keyword}%") ->whereOr('service_name', 'like', "%{$keyword}%") ->whereOr('address', 'like', "%{$keyword}%") ->whereOr('remark', 'like', "%{$keyword}%"); }); } if ($status === 'active') { $q->where('status', 'in', \app\common\library\OrderStatus::activeValues()); } elseif ($status === 'completed') { $q->where('status', \app\common\library\OrderStatus::COMPLETED); } elseif ($status === 'cancelled') { $q->where('status', \app\common\library\OrderStatus::CANCELLED); } elseif ($status !== 'all' && $status !== '') { $q->where('status', (int)$status); } return $q; }; $total = $build()->count(); $rows = $build()->order('id desc')->page($page, $limit)->select(); $userMap = Db::name('user')->where('id', 'in', $uids)->column('id,nickname,mobile', 'id'); $serviceIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'service_id'))))); $serviceImgMap = $serviceIds ? Db::name('service')->where('id', 'in', $serviceIds)->column('image', 'id') : []; $orderIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'id'))))); $batchMap = $orderIds ? Db::name('merchant_order')->where('order_id', 'in', $orderIds)->column('batch_no', 'order_id') : []; foreach ($rows as &$row) { $ownerUser = (int)$row['user_id'] === (int)$merchant['user_id']; $u = isset($userMap[(int)$row['user_id']]) ? $userMap[(int)$row['user_id']] : null; $mobile = $u ? (string)$u['mobile'] : ''; $custMobile = (string)$row['mobile']; $row['mobile_mask'] = strlen($custMobile) === 11 ? substr_replace($custMobile, '****', 3, 4) : $custMobile; $row['pay_amount'] = (float)$row['pay_amount']; $row['urgent'] = (int)$row['urgent']; $row['status_text'] = \app\common\library\OrderStatus::label((int)$row['status']); $row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门'; $row['create_text'] = date('Y-m-d H:i', (int)$row['createtime']); $row['address_text'] = $row['province'] . $row['city'] . $row['district'] . $row['address']; $row['account_name'] = $u && $u['nickname'] ? $u['nickname'] : ('用户' . substr($mobile, -4)); $row['account_role'] = $ownerUser ? 'owner' : 'member'; $row['account_role_text'] = $ownerUser ? '主账号' : '关联账号'; $row['batch_no'] = isset($batchMap[(int)$row['id']]) ? (string)$batchMap[(int)$row['id']] : ''; $img = !empty($row['service_image']) ? $row['service_image'] : ''; if (empty($img) && !empty($row['service_id']) && isset($serviceImgMap[(int)$row['service_id']])) { $img = $serviceImgMap[(int)$row['service_id']]; } if ($img && strpos($img, 'http') !== 0) { $img = cdnurl($img, true); } $row['service_image'] = $img; $row['goods_image_url'] = $img; } unset($row); $this->success('', [ 'list' => $rows, 'total' => (int)$total, 'page' => $page, 'has_more' => ($page * $limit) < (int)$total, ]); } /** * 开票记录与可开票额度(个人用户与认证商家通用,兼容旧版调用并复用 InvoiceService) */ public function invoices() { $records = InvoiceService::getRecordList($this->uid(), 'all', 1, 30); $quota = InvoiceService::getQuotaSummary($this->uid()); $this->success('', [ 'list' => $records['list'], 'quota' => $quota, ]); } /** * 申请开票(向下兼容旧版入口,底层复用 InvoiceService) * @ApiMethod (POST) */ public function invoice() { $params = [ 'order_ids' => $this->request->post('order_ids', '', null), 'amount' => $this->request->post('amount', 0), 'title_id' => $this->request->post('title_id', 0), 'type' => $this->request->post('type', 'normal'), 'title_type' => $this->request->post('title_type', ''), 'title' => $this->request->post('title', ''), 'tax_no' => $this->request->post('tax_no', ''), 'bank_name' => $this->request->post('bank_name', ''), 'bank_account' => $this->request->post('bank_account', ''), 'reg_address' => $this->request->post('reg_address', ''), 'reg_mobile' => $this->request->post('reg_mobile', ''), 'email' => $this->request->post('email', ''), 'receive_mobile' => $this->request->post('receive_mobile', ''), 'remark' => $this->request->post('remark', ''), 'save_title' => $this->request->post('save_title', 0), ]; try { $result = InvoiceService::submitInvoice($this->uid(), $params, false); } catch (\InvalidArgumentException $e) { $this->error($e->getMessage()); return; } catch (\Exception $e) { \think\Log::error('[Merchant::invoice] ' . $e->getMessage()); $this->error('开票申请提交失败,请稍后重试'); return; } $this->success('开票申请已提交,3 个工作日内开具', $result); } /** * 商家关联账号列表(主账号 + 已绑定账号) */ public function members() { $merchant = $this->requireMerchant(); $uids = $this->merchantUserIds($merchant); $isOwner = (int)$merchant['user_id'] === $this->uid(); $users = Db::name('user')->where('id', 'in', $uids)->column('id,nickname,mobile,avatar', 'id'); $bindRows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->select(); $bindMap = []; foreach ($bindRows as $b) { $bindMap[(int)$b['user_id']] = $b; } $stats = Db::name('order')->field('user_id,count(*) as cnt,sum(pay_amount) as amount') ->where('user_id', 'in', $uids)->where('pay_status', 1) ->group('user_id')->select(); $statMap = []; foreach ($stats as $s) { $statMap[(int)$s['user_id']] = $s; } $list = []; foreach ($uids as $uid) { $u = isset($users[$uid]) ? $users[$uid] : null; $owner = (int)$uid === (int)$merchant['user_id']; $bind = isset($bindMap[(int)$uid]) ? $bindMap[(int)$uid] : null; $mobile = $u ? (string)$u['mobile'] : ''; $list[] = [ 'id' => $owner ? 0 : (int)$bind['id'], 'user_id' => (int)$uid, 'nickname' => $u ? ($u['nickname'] ?: ('用户' . substr($mobile, -4))) : '用户', 'mobile' => $mobile !== '' ? substr_replace($mobile, '****', 3, 4) : '', 'avatar' => $u ? $u['avatar'] : '', 'role' => $owner ? 'owner' : 'member', 'role_text' => $owner ? '主账号' : '关联账号', 'is_owner' => $owner, 'order_count' => isset($statMap[(int)$uid]) ? (int)$statMap[(int)$uid]['cnt'] : 0, 'total_amount' => isset($statMap[(int)$uid]) ? round((float)$statMap[(int)$uid]['amount'], 2) : 0, 'bind_time_text' => (!$owner && $bind) ? date('Y-m-d H:i', (int)$bind['createtime']) : '', ]; } // 主账号排在最前 usort($list, function ($a, $b) { return (int)$b['is_owner'] - (int)$a['is_owner']; }); $this->success('', [ 'list' => $list, 'is_owner' => $isOwner, 'total' => count($list), ]); } /** * 绑定关联账号(仅主账号) * @ApiMethod (POST) */ public function bindMember() { $merchant = $this->requireMerchant(); $this->requireMerchantOwner($merchant); $mobile = trim((string)$this->request->post('mobile')); if (!preg_match('/^1\d{10}$/', $mobile)) { $this->error('请输入正确的关联账号手机号'); } $user = Db::name('user')->where('mobile', $mobile)->find(); if (!$user) { $this->error('该手机号尚未注册,请对方先登录小程序'); } if ((int)$user['id'] === $this->uid()) { $this->error('不能绑定当前主账号'); } if (Db::name('merchant')->where('user_id', $user['id'])->find()) { $this->error('该账号已是商家主账号,不能重复绑定'); } if (Db::name('merchant_member')->where('user_id', $user['id'])->find()) { $this->error('该账号已绑定其他商家'); } $id = Db::name('merchant_member')->insertGetId([ 'merchant_id' => (int)$merchant['id'], 'user_id' => (int)$user['id'], 'remark' => '', 'createtime' => time(), 'updatetime' => time(), ]); try { $this->notify('user', (int)$user['id'], '已加入商家账号', '您的账号已关联到商家「' . $merchant['name'] . '」,下单、账单、开票数据将实时同步', 'system', 0); } catch (\Exception $e) { \think\Log::error('[Merchant::bindMember notify] ' . $e->getMessage()); } $this->success('绑定成功,关联账号数据将实时同步', [ 'member_id' => (int)$id, 'user_id' => (int)$user['id'], 'nickname' => $user['nickname'] ?: ('用户' . substr($mobile, -4)), ]); } /** * 解除关联账号(仅主账号) * @ApiMethod (POST) */ public function unbindMember() { $merchant = $this->requireMerchant(); $this->requireMerchantOwner($merchant); $memberId = (int)$this->request->post('member_id'); $userId = (int)$this->request->post('user_id'); $query = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id']); if ($memberId > 0) { $query->where('id', $memberId); } elseif ($userId > 0) { $query->where('user_id', $userId); } else { $this->error('缺少关联账号参数'); } $row = $query->find(); if (!$row) { $this->error('关联账号不存在或已解除'); } if ((int)$row['user_id'] === (int)$merchant['user_id']) { $this->error('不能解除主账号'); } Db::name('merchant_member')->where('id', (int)$row['id'])->delete(); try { $this->notify('user', (int)$row['user_id'], '商家关联已解除', '您已与商家「' . $merchant['name'] . '」解除关联,对方商家数据不再同步', 'system', 0); } catch (\Exception $e) { \think\Log::error('[Merchant::unbindMember notify] ' . $e->getMessage()); } $this->success('已解除关联,对方将不再同步商家数据'); } /** * 批量下单后绑定批次(订单本身走已验收的 /order/create) * @ApiMethod (POST) */ public function bindBatch() { $merchant = $this->requireMerchant(); $uids = $this->merchantUserIds($merchant); // 兼容三种入参:数组 / 逗号串 / 混合 $raw = $this->request->post('order_ids'); $orderIds = []; foreach ((array)$raw as $v) { foreach (explode(',', (string)$v) as $one) { $one = (int)$one; if ($one > 0) { $orderIds[] = $one; } } } $orderIds = array_values(array_unique($orderIds)); $batchNo = trim((string)$this->request->post('batch_no', '')); if (empty($orderIds)) { $this->error('缺少订单'); } if ($batchNo === '') { $batchNo = date('YmdHis') . mt_rand(100, 999); } $ok = 0; foreach ($orderIds as $oid) { $oid = (int)$oid; if (!Db::name('order')->where('id', $oid)->where('user_id', 'in', $uids)->find()) { continue; } if (Db::name('merchant_order')->where('order_id', $oid)->find()) { continue; } Db::name('merchant_order')->insert([ 'merchant_id' => (int)$merchant['id'], 'order_id' => $oid, 'batch_no' => $batchNo, 'createtime' => time(), ]); $ok++; } $this->success('已归属到批次', ['batch_no' => $batchNo, 'count' => $ok]); } /** * 当前用户商家档案:主账号直接匹配,关联账号通过绑定关系匹配 */ /** * 商户端:Excel 或结构化数据一键批量解析下单 * @ApiMethod (POST) */ public function importBatch() { $merchant = $this->requireMerchant(); $rawOrders = $this->request->post('orders', null, null); if ($rawOrders === null || $rawOrders === '' || $rawOrders === []) { $rawOrders = $this->request->post('items', [], null); } $items = []; if (is_string($rawOrders)) { $decoded = json_decode(htmlspecialchars_decode(trim($rawOrders)), true); if (is_array($decoded)) { $items = $decoded; } } elseif (is_array($rawOrders)) { if (count($rawOrders) === 1 && isset($rawOrders[0]) && is_string($rawOrders[0])) { $decoded = json_decode(htmlspecialchars_decode(trim($rawOrders[0])), true); $items = is_array($decoded) ? $decoded : $rawOrders; } else { $items = $rawOrders; } } if (empty($items) || !is_array($items)) { $this->error('导入数据不能为空'); } if (count($items) > 100) { $this->error('单次批量导入最多支持 100 条订单'); } $globalServiceId = (int)$this->request->post('service_id', 0); $serviceCache = []; $preparedRows = []; foreach ($items as $idx => $item) { if (is_string($item)) { $decItem = json_decode(htmlspecialchars_decode(trim($item)), true); if (is_array($decItem)) { $item = $decItem; } } if (!is_array($item)) { continue; } $contact = trim((string)(isset($item['contact']) ? $item['contact'] : '买家')); $mobile = preg_replace('/[^\d]/', '', trim((string)(isset($item['mobile']) ? $item['mobile'] : ''))); $address = trim((string)(isset($item['address']) ? $item['address'] : '')); if ($mobile === '' && $address === '') { continue; } $rowNo = $idx + 1; if (!preg_match('/^1\d{10}$/', $mobile)) { $this->error("第 {$rowNo} 条订单联系电话格式不正确"); } if ($address === '') { $this->error("第 {$rowNo} 条订单缺少详细服务地址"); } $itemServiceId = !empty($item['service_id']) ? (int)$item['service_id'] : $globalServiceId; if ($itemServiceId <= 0) { $this->error("第 {$rowNo} 条订单未指定有效的服务项目"); } if (!array_key_exists($itemServiceId, $serviceCache)) { $serviceCache[$itemServiceId] = Db::name('service') ->where('id', $itemServiceId) ->where('status', 'normal') ->find() ?: null; } $svc = $serviceCache[$itemServiceId]; if (!$svc) { $this->error("第 {$rowNo} 条订单关联的服务项目不存在或已下架"); } if ($svc['price_type'] === 'quote') { $this->error("第 {$rowNo} 条订单所选服务「{$svc['name']}」为师傅报价项目,不支持批量一口价导入"); } $goodsNum = max(1, (int)(isset($item['goods_num']) ? $item['goods_num'] : (isset($item['num']) ? $item['num'] : 1))); $floor = isset($item['floor']) ? max(1, (int)$item['floor']) : 1; $hasElevator = isset($item['has_elevator']) ? (int)$item['has_elevator'] : (isset($item['elevator']) ? (int)$item['elevator'] : 1); $urgent = !empty($item['urgent']) ? 1 : 0; $specId = isset($item['spec_id']) ? (int)$item['spec_id'] : 0; $reqServiceType = isset($item['service_type']) ? trim((string)$item['service_type']) : ''; // 严禁采信前端传入的 price,强制根据数据库 fa_service / fa_service_spec 与楼层规则计算真实金额 try { $calc = Home::calcAmount($svc, [ 'floor' => $floor, 'has_elevator' => $hasElevator ? 1 : 0, 'urgent' => $urgent, 'goods_num' => $goodsNum, 'spec_id' => $specId, 'service_type' => $reqServiceType, 'strict' => true, ], 0, $this->uid(), 'fixed', 0, []); } catch (\InvalidArgumentException $e) { $this->error("第 {$rowNo} 条订单计价校验失败:" . $e->getMessage()); return; } if ((float)$calc['pay_amount'] <= 0) { $this->error("第 {$rowNo} 条订单计算金额异常"); } $resolvedProvince = mb_substr(trim((string)(isset($item['province']) && $item['province'] !== '' ? $item['province'] : ($merchant['province'] ?: ''))), 0, 50); $resolvedCity = mb_substr(trim((string)(isset($item['city']) && $item['city'] !== '' ? $item['city'] : ($merchant['city'] ?: ''))), 0, 50); if ($resolvedProvince === '' || $resolvedCity === '') { $this->error("第 {$rowNo} 条订单缺少服务省份或城市信息,请在Excel或表格中填写后再提交"); } $preparedRows[] = [ 'svc' => $svc, 'service_id' => (int)$svc['id'], 'cat_id' => (int)$svc['cat_id'], 'service_name' => (string)$svc['name'], 'service_img' => (string)$svc['image'], 'goods_model' => mb_substr(trim((string)(isset($item['goods_model']) ? $item['goods_model'] : '')), 0, 100), 'goods_num' => $goodsNum, 'contact' => mb_substr($contact ?: '买家', 0, 50), 'mobile' => $mobile, 'province' => $resolvedProvince, 'city' => $resolvedCity, 'district' => mb_substr(trim((string)(isset($item['district']) ? $item['district'] : ($merchant['district'] ?: ''))), 0, 50), 'address' => mb_substr($address, 0, 255), 'floor' => $floor, 'has_elevator' => $hasElevator ? 1 : 0, 'urgent' => $urgent, 'remark' => mb_substr(trim((string)(isset($item['remark']) ? $item['remark'] : '')), 0, 500), 'appoint_time' => !empty($item['appoint_time']) ? (int)$item['appoint_time'] : null, 'calc' => $calc, ]; } if (empty($preparedRows)) { $this->error('未解析到有效的导入订单数据'); } $ordersCreated = []; $orderIds = []; $totalAmount = 0.00; $isMonthly = false; $batchNo = 'MB' . date('YmdHis') . mt_rand(1000, 9999); $now = time(); // 事务外一次性预加载生效抽佣规则与默认兜底配置,循环内纯内存匹配快照(零额外 SQL) $activeCommRules = CommissionService::getActiveRules(); $commDefaults = FinanceConfigService::getCommissionDefaults(); Db::startTrans(); try { $lockedMerchant = Db::name('merchant')->where('id', (int)$merchant['id'])->lock(true)->find(); if (!$lockedMerchant || $lockedMerchant['verify_status'] !== 'pass') { throw new \InvalidArgumentException('商家资质状态异常,无法批量发单'); } $isMonthly = ($lockedMerchant['settle_type'] === 'monthly'); $initStatus = $isMonthly ? OrderStatus::PENDING_ACCEPT : OrderStatus::PENDING_PAY; $initPayStatus = $isMonthly ? 1 : 0; $initPayType = $isMonthly ? 'monthly' : ''; $initPayTime = $isMonthly ? $now : null; foreach ($preparedRows as $pRow) { $orderNo = $this->makeOrderNo(); $verifyCode = str_pad((string)mt_rand(0, 999999), 6, '0', STR_PAD_LEFT); $calc = $pRow['calc']; $payAmt = round((float)$calc['pay_amount'], 2); $unitPrice = round((float)$calc['base_amount'] / max(1, (int)$pRow['goods_num']), 2); $commMatch = CommissionService::matchOrderSnapshot([ 'price_type' => 'fixed', 'customer_type' => 'merchant', 'cat_id' => (int)$pRow['cat_id'], 'urgent' => (int)$pRow['urgent'], 'order_amount' => $payAmt, ], $activeCommRules, $commDefaults); $orderId = (int)Db::name('order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $this->uid(), 'master_id' => 0, 'cat_id' => $pRow['cat_id'], 'service_id' => $pRow['service_id'], 'service_name' => $pRow['service_name'], 'service_image' => $pRow['service_img'], 'price_type' => 'fixed', 'goods_model' => $pRow['goods_model'], 'goods_num' => $pRow['goods_num'], 'contact' => $pRow['contact'], 'mobile' => $pRow['mobile'], 'province' => $pRow['province'], 'city' => $pRow['city'], 'district' => $pRow['district'], 'address' => $pRow['address'], 'floor' => $pRow['floor'], 'has_elevator' => $pRow['has_elevator'], 'urgent' => $pRow['urgent'], 'remark' => $pRow['remark'], 'appoint_time' => $pRow['appoint_time'], 'base_amount' => $calc['base_amount'], 'floor_amount' => $calc['floor_amount'], 'urgent_amount' => $calc['urgent_amount'], 'discount_amount' => 0.00, 'total_amount' => $calc['total_amount'], 'pay_amount' => $payAmt, 'commission_rate' => $commMatch['commission_rate'], 'commission_snapshot' => $commMatch['commission_snapshot'], 'status' => $initStatus, 'pay_status' => $initPayStatus, 'pay_type' => $initPayType, 'pay_time' => $initPayTime, 'transaction_id' => $isMonthly ? ('MONTHLY' . date('YmdHis', $now) . mt_rand(1000, 9999)) : '', 'verify_code' => $verifyCode, 'createtime' => $now, 'updatetime' => $now, ]); Db::name('order_goods')->insert([ 'order_id' => $orderId, 'service_id' => $pRow['service_id'], 'goods_name' => $pRow['service_name'], 'goods_model' => $pRow['goods_model'], 'goods_num' => $pRow['goods_num'], 'goods_image' => $pRow['service_img'], 'addon_services' => '', 'base_price' => $unitPrice, 'createtime' => $now, ]); Db::name('merchant_order')->insert([ 'merchant_id' => (int)$lockedMerchant['id'], 'order_id' => $orderId, 'batch_no' => $batchNo, 'createtime' => $now, ]); $this->addLog( $orderId, $initStatus, $isMonthly ? "企业批量导单(月结授信,批次 {$batchNo}),已进入接单大厅" : "企业批量导单创建(待支付,批次 {$batchNo})", 'user', $this->uid() ); $orderIds[] = $orderId; $ordersCreated[] = [ 'order_id' => $orderId, 'order_no' => $orderNo, 'contact' => $pRow['contact'], 'amount' => $payAmt, ]; $totalAmount = round($totalAmount + $payAmt, 2); } if ($isMonthly) { Db::name('merchant')->where('id', (int)$lockedMerchant['id'])->update([ 'order_count' => (int)$lockedMerchant['order_count'] + count($ordersCreated), 'total_amount' => round((float)$lockedMerchant['total_amount'] + $totalAmount, 2), 'updatetime' => $now, ]); } Db::commit(); } catch (\think\exception\HttpResponseException $e) { Db::rollback(); throw $e; } catch (\InvalidArgumentException $e) { Db::rollback(); $this->error($e->getMessage()); return; } catch (\Exception $e) { Db::rollback(); \think\Log::error('[Merchant::importBatch] ' . $e->getMessage()); $this->error('批量导入失败:' . $e->getMessage()); return; } if ($isMonthly && !empty($orderIds)) { try { $masters = Db::name('master') ->where('verify_status', 'pass') ->where('work_status', 'on') ->where('user_id', '<>', $this->uid()) ->limit(20) ->select(); $cnt = count($orderIds); foreach ($masters as $m) { $this->notify('master', (int)$m['id'], '企业批量新工单就绪', "商家刚发布了 {$cnt} 笔新工单进入接单大厅,请尽快前往查看抢单!", 'order', (int)$orderIds[0]); } } catch (\Exception $e) { \think\Log::error('[Merchant::importBatch broadcast] ' . $e->getMessage()); } } $this->success($isMonthly ? '批量下单成功,已按月结授信进入接单大厅!' : '批量导入成功,请完成批次支付', [ 'need_pay' => !$isMonthly, 'settle_type' => $isMonthly ? 'monthly' : 'prepay', 'batch_no' => $batchNo, 'count' => count($ordersCreated), 'total_amount' => round($totalAmount, 2), 'order_ids' => $orderIds, 'orders' => $ordersCreated, ]); } /** * 商户端:预付商家按批次号支付待支付批量订单(严禁模拟免付兜底,预付订单须逐笔走真实微信支付) * @ApiMethod (POST) */ public function payBatch() { $this->requireMerchant(); $this->error('系统已全面禁用模拟支付通道,企业预付工单请逐笔使用真实微信支付完成付款(月结授信企业导单时已直接授信进入大厅)'); } protected function merchantInfo() { $merchant = Db::name('merchant')->where('user_id', $this->uid())->find(); if ($merchant) { return $merchant; } $bind = Db::name('merchant_member')->where('user_id', $this->uid())->find(); if ($bind) { return Db::name('merchant')->where('id', (int)$bind['merchant_id'])->find(); } return null; } /** * 商家数据归属用户集合:主账号 + 所有关联账号 */ protected function merchantUserIds($merchant) { $ids = [(int)$merchant['user_id']]; $rows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->column('user_id'); foreach ($rows as $id) { $id = (int)$id; if ($id > 0 && !in_array($id, $ids, true)) { $ids[] = $id; } } return $ids; } /** * 仅商家主账号可执行敏感操作 */ protected function requireMerchantOwner($merchant) { if ((int)$merchant['user_id'] !== $this->uid()) { $this->error('仅主账号可执行此操作', null, 403); } } /** * 要求已通过审核的商家 */ protected function requireMerchant() { $m = $this->merchantInfo(); if (!$m) { $curUser = Db::name('user')->where('id', $this->uid())->find(); if ($curUser && !empty($curUser['user_type']) && $curUser['user_type'] === 'merchant') { $now = time(); $newId = Db::name('merchant')->insertGetId([ 'user_id' => $this->uid(), 'name' => !empty($curUser['nickname']) ? (string)$curUser['nickname'] : '商家用户', 'license_no' => '', 'contact' => (string)$curUser['nickname'], 'mobile' => (string)$curUser['mobile'], 'address' => isset($curUser['contact_address']) ? (string)$curUser['contact_address'] : '', 'email' => isset($curUser['email']) ? (string)$curUser['email'] : '', 'verify_status' => 'pass', 'createtime' => $now, 'updatetime' => $now, ]); return Db::name('merchant')->where('id', $newId)->find(); } $this->error('请先在「账号信息」切换为商家类型', null, 2001); } if ($m['verify_status'] !== 'pass') { Db::name('merchant')->where('id', (int)$m['id'])->update([ 'verify_status' => 'pass', 'updatetime' => time(), ]); $m['verify_status'] = 'pass'; } return $m; } }