文件
yjyf/server/application/api/controller/Merchant.php
linda 4e73c8164a fix(wxlogin): 修复微信登录 40029 + 新增商家(企业账号)全流程
微信登录修复:
- 根因:manifest.json 的 mp-weixin.appid 为空 → 开发者工具用测试号,code 与服务端 appid 不匹配
- 修复:填入 wx40ca4b02d4d1cb58
- WxLogin 增加错误码语义映射(40029 appid 不一致 / 40125 secret 错 / 45011 频繁 / 40226 风控)
- 登录页新增手机号登录兜底通道(微信不可用时业务不被卡住)

商家(企业账号):
- 新表 fa_merchant / fa_merchant_order / fa_invoice(纯新增,DB 40→45 表)
- Merchant 控制器:apply/info/update/bills/invoices/invoice/bindBatch
  入驻含企业信息+营业执照+开票信息+结算方式(预付/月结)
  账单按月汇总、开票额度按已付-已开票校验、批量下单按批次归属
- Auth::switchRole 支持 merchant(2001/2002/2003 语义码 + redirect)
- Auth::profile 增加 is_merchant / merchant

前端:
- 登录页角色扩为 业主/师傅/商家 三类
- pages/merchant/apply 企业入驻;workbench 商家工作台(经营数据+按月账单+近期订单+六宫格)
- pages/merchant/batch 批量下单(多地址×多数量)→ 逐单建单支付 → 绑定批次
- pages/merchant/invoice 开票申请与记录
- pages/mine 增加商家入驻/切换商家端入口;角色徽章三态

验证:163 项断言全通过(page_flow 47 + e2e 65 + phase2 27 + phase3 24)
2026-10-28 16:22:20 +08:00

293 行
11 KiB
PHP

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

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

<?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';
}
$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;
}
$uid = $this->uid();
$orderCount = (int)Db::name('order')->where('user_id', $uid)->where('pay_status', 1)->count();
$totalAmount = (float)Db::name('order')->where('user_id', $uid)->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,
'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'],
'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();
$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()
{
$this->requireMerchant();
$uid = $this->uid();
$months = Db::name('order')
->field("FROM_UNIXTIME(createtime,'%Y-%m') as month, count(*) as cnt, sum(pay_amount) as amount")
->where('user_id', $uid)->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', $uid)->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 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', $this->uid())->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 个工作日内开具');
}
/**
* 批量下单后绑定批次(订单本身走已验收的 /order/create)
* @ApiMethod (POST)
*/
public function bindBatch()
{
$merchant = $this->requireMerchant();
// 兼容三种入参:数组 / 逗号串 / 混合
$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', $this->uid())->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()
{
return Db::name('merchant')->where('user_id', $this->uid())->find();
}
/**
* 要求已通过审核的商家
*/
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;
}
}