- 新增 /merchant/orders,按主账号+关联账号聚合订单并支持状态筛选 - 新增企业订单页,展示订单归属账号(主账号/关联账号) - 工作台企业订单入口改到聚合列表,近期订单可点击查看 - 订单详情允许同商家账号查看,非下单账号隐藏操作按钮 - 报价选择仅下单账号可操作,防止关联账号/主账号误操作
531 行
21 KiB
PHP
531 行
21 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
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)->count();
|
||
$totalAmount = (float)Db::name('order')->where('user_id', 'in', $uids)->where('pay_status', 1)->sum('pay_amount');
|
||
$invoiced = (float)Db::name('invoice')->where('merchant_id', $merchant['id'])->where('status', 'issued')->sum('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'],
|
||
'address' => $merchant['province'] . $merchant['city'] . $merchant['district'] . $merchant['address'],
|
||
'invoice_title' => $merchant['invoice_title'],
|
||
'invoice_tax_no' => $merchant['invoice_tax_no'],
|
||
'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),
|
||
'invoiceable' => round(max(0, $totalAmount - $invoiced), 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);
|
||
$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');
|
||
$page = max(1, (int)$this->request->param('page', 1));
|
||
$limit = min(50, max(1, (int)$this->request->param('limit', 20)));
|
||
|
||
$build = function () use ($uids, $status) {
|
||
$q = Db::name('order')->where('user_id', 'in', $uids);
|
||
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');
|
||
|
||
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'] : '';
|
||
$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 ? '主账号' : '关联账号';
|
||
}
|
||
unset($row);
|
||
|
||
$this->success('', [
|
||
'list' => $rows,
|
||
'total' => (int)$total,
|
||
'page' => $page,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 开票记录
|
||
*/
|
||
public function invoices()
|
||
{
|
||
$merchant = $this->requireMerchant();
|
||
$list = Db::name('invoice')->where('merchant_id', $merchant['id'])->order('id desc')->limit(30)->select();
|
||
$statusText = ['wait' => '开票中', 'issued' => '已开票', 'reject' => '已驳回'];
|
||
foreach ($list as &$row) {
|
||
$row['amount'] = (float)$row['amount'];
|
||
$row['status_text'] = isset($statusText[$row['status']]) ? $statusText[$row['status']] : $row['status'];
|
||
$row['type_text'] = $row['type'] === 'special' ? '专用发票' : '普通发票';
|
||
$row['time_text'] = date('Y-m-d H:i', (int)$row['createtime']);
|
||
}
|
||
unset($row);
|
||
$this->success('', ['list' => $list]);
|
||
}
|
||
|
||
/**
|
||
* 申请开票
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function invoice()
|
||
{
|
||
$merchant = $this->requireMerchant();
|
||
$amount = round((float)$this->request->post('amount'), 2);
|
||
$type = $this->request->post('type', 'normal');
|
||
$email = trim((string)$this->request->post('email'));
|
||
|
||
if ($amount <= 0) {
|
||
$this->error('请填写开票金额');
|
||
}
|
||
if (!in_array($type, ['normal', 'special'], true)) {
|
||
$this->error('发票类型错误');
|
||
}
|
||
|
||
$paid = (float)Db::name('order')->where('user_id', 'in', $this->merchantUserIds($merchant))->where('pay_status', 1)->sum('pay_amount');
|
||
$used = (float)Db::name('invoice')->where('merchant_id', $merchant['id'])->where('status', 'in', ['wait', 'issued'])->sum('amount');
|
||
$available = round(max(0, $paid - $used), 2);
|
||
if ($amount > $available) {
|
||
$this->error('可开票金额不足,当前可开 ¥' . $available);
|
||
}
|
||
|
||
Db::name('invoice')->insert([
|
||
'merchant_id' => (int)$merchant['id'],
|
||
'user_id' => $this->uid(),
|
||
'amount' => $amount,
|
||
'type' => $type,
|
||
'title' => $merchant['invoice_title'] ?: $merchant['name'],
|
||
'tax_no' => $merchant['invoice_tax_no'],
|
||
'email' => $email,
|
||
'status' => 'wait',
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
|
||
$this->success('开票申请已提交,3 个工作日内开具');
|
||
}
|
||
|
||
/**
|
||
* 商家关联账号列表(主账号 + 已绑定账号)
|
||
*/
|
||
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) {}
|
||
|
||
$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) {}
|
||
$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]);
|
||
}
|
||
|
||
/**
|
||
* 当前用户商家档案:主账号直接匹配,关联账号通过绑定关系匹配
|
||
*/
|
||
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) {
|
||
$this->error('请先完成商家入驻', ['need_apply' => 1], 2001);
|
||
}
|
||
if ($m['verify_status'] === 'pending') {
|
||
$this->error('商家资质审核中,请耐心等待', ['verify_status' => 'pending'], 2002);
|
||
}
|
||
if ($m['verify_status'] === 'reject') {
|
||
$this->error('资质未通过:' . $m['audit_msg'], ['verify_status' => 'reject'], 2003);
|
||
}
|
||
return $m;
|
||
}
|
||
}
|