feat: 新增发票抬头、晒单、钱包日志等模型及后台管理功能

- 新增 invoice_title、showcase、wallet_log 模型及对应语言包
- 新增后台控制器、视图及搜索字段配置
- 修复服务类目删除保护逻辑
- 优化后台登录页面文案及样式
- 新增前端角色切换逻辑及页面拦截
- 新增 SVG 图标及样式变量
- 修复接口权限及下拉框默认值问题
这个提交包含在:
2026-11-11 17:02:35 +08:00
父节点 5908cf6b16
当前提交 fafddceaf2
共有 332 个文件被更改,包括 33892 次插入 和 8835 次删除

查看文件

@@ -0,0 +1,233 @@
<?php
namespace app\api\controller;
use app\common\library\InvoiceService;
use think\Log;
/**
* 发票中心(个人业主与企业商家通用):
* 额度汇总 / 按单勾选开票 / 发票抬头管理 / 开票记录与详情 / 撤回申请
*/
class Invoice extends Base
{
/**
* 开票首页概览(可开票额度、默认抬头、未开票订单数、记录统计)
*/
public function summary()
{
$quota = InvoiceService::getQuotaSummary($this->uid());
$titles = InvoiceService::getTitleList($this->uid());
$this->success('', [
'quota' => $quota,
'titles' => $titles,
]);
}
/**
* 可开票已完成订单列表(支持分页或传入 order_ids 核对已勾选订单)
*/
public function orders()
{
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(100, max(1, (int)$this->request->param('limit', 20)));
$rawIds = $this->request->param('order_ids/a', []);
$filterIds = InvoiceService::parseIds($rawIds);
$res = InvoiceService::getInvoiceableOrders($this->uid(), $page, $limit, $filterIds);
$this->success('', $res);
}
/**
* 提交开票申请(支持按勾选订单合并开票或按可用额度开票,严格校验专票要素)
* @ApiMethod (POST)
*/
public function apply()
{
$params = [
'order_ids' => $this->request->post('order_ids/a', []),
'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, true);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
} catch (\Exception $e) {
Log::error('[Invoice::apply] ' . $e->getMessage());
$this->error('提交开票申请失败,请稍后重试');
return;
}
$this->success('开票申请已提交,预计 1-3 个工作日内开具', $result);
}
/**
* 开票记录分页列表
*/
public function records()
{
$status = trim((string)$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)));
$res = InvoiceService::getRecordList($this->uid(), $status, $page, $limit);
$quota = InvoiceService::getQuotaSummary($this->uid());
$res['quota'] = [
'available' => $quota['available'],
'paid_amount' => $quota['paid_amount'],
'used_amount' => $quota['used_amount'],
'issued_amount' => $quota['issued_amount'],
'wait_count' => $quota['wait_count'],
'record_count' => $quota['record_count'],
];
$this->success('', $res);
}
/**
* 开票申请详情(含完整票面信息、电子发票附件、驳回说明、关联订单明细)
*/
public function detail()
{
$id = (int)$this->request->param('id', 0);
try {
$detail = InvoiceService::getInvoiceDetail($this->uid(), $id);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
}
$this->success('', ['detail' => $detail]);
}
/**
* 撤回待处理(wait)开票申请
* @ApiMethod (POST)
*/
public function cancel()
{
$id = (int)$this->request->post('id', 0);
try {
InvoiceService::cancelInvoice($this->uid(), $id);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
} catch (\Exception $e) {
Log::error('[Invoice::cancel] ' . $e->getMessage());
$this->error('撤回失败,请刷新后重试');
return;
}
$this->success('已撤回开票申请,可开票额度已释放');
}
/**
* 发票抬头列表
*/
public function titles()
{
$list = InvoiceService::getTitleList($this->uid());
$this->success('', ['list' => $list]);
}
/**
* 发票抬头详情
*/
public function titleDetail()
{
$id = (int)$this->request->param('id', 0);
try {
$detail = InvoiceService::getOwnTitle($this->uid(), $id);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
}
$this->success('', ['detail' => $detail]);
}
/**
* 新增 / 编辑发票抬头
* @ApiMethod (POST)
*/
public function titleSave()
{
$input = [
'id' => (int)$this->request->post('id', 0),
'title_type' => $this->request->post('title_type', 'personal'),
'default_invoice_type' => $this->request->post('default_invoice_type', 'normal'),
'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', ''),
'is_default' => (int)$this->request->post('is_default', 0),
];
try {
$saved = InvoiceService::saveTitle($this->uid(), $input);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
} catch (\Exception $e) {
Log::error('[Invoice::titleSave] ' . $e->getMessage());
$this->error('保存失败,请稍后重试');
return;
}
$this->success('保存成功', ['detail' => $saved, 'id' => (int)$saved['id']]);
}
/**
* 设为默认发票抬头
* @ApiMethod (POST)
*/
public function titleDefault()
{
$id = (int)$this->request->post('id', 0);
try {
InvoiceService::setDefaultTitle($this->uid(), $id);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
} catch (\Exception $e) {
Log::error('[Invoice::titleDefault] ' . $e->getMessage());
$this->error('设置失败,请稍后重试');
return;
}
$this->success('已设为默认抬头');
}
/**
* 删除发票抬头
* @ApiMethod (POST)
*/
public function titleDelete()
{
$id = (int)$this->request->post('id', 0);
try {
InvoiceService::deleteTitle($this->uid(), $id);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
return;
} catch (\Exception $e) {
Log::error('[Invoice::titleDelete] ' . $e->getMessage());
$this->error('删除失败,请稍后重试');
return;
}
$this->success('已删除');
}
}