932 行
41 KiB
PHP
932 行
41 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\library\CommissionService;
|
||
use app\common\library\FinanceConfigService;
|
||
use app\common\library\InvoiceService;
|
||
use app\common\library\OrderStatus;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 商家端(企业账号):入驻 / 资料 / 账单 / 开票 / 批量下单归属
|
||
*/
|
||
class Merchant extends Base
|
||
{
|
||
/**
|
||
* 商家入驻 / 重新提交
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function apply()
|
||
{
|
||
$data = [
|
||
'name' => 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;
|
||
}
|
||
}
|