Bladeren bron

✨ feat(finance): 新增 VCC 返点/提成付款申请及类别管理功能

- 新增 VCCPaymentCate 和 VCCPayment 控制器/模型,支持类别及申请的增删查改操作。
- 新增 datalist / add / view 三个前端页面。
- 新增建表迁移 create_vcc_payment 和 create_vcc_payment_cate。
- 新增填充迁移 seed_vcc_payment_data / seed_vcc_payment_cate_data / seed_vcc_payment_cate_rule。
- 注册审批流程、消息模板、权限节点及操作权限。
- 新增相关视图并优化前端交互与审批逻辑。
lxz 2 weken geleden
bovenliggende
commit
61b5e98773
28 gewijzigde bestanden met toevoegingen van 1732 en 45 verwijderingen
  1. 1 1
      app/api/controller/Import.php
  2. 132 0
      app/finance/controller/VccPayment.php
  3. 76 0
      app/finance/controller/VccPaymentCate.php
  4. 90 0
      app/finance/model/VccPayment.php
  5. 71 0
      app/finance/model/VccPaymentCate.php
  6. 31 0
      app/finance/validate/VccPaymentValidate.php
  7. 2 8
      app/finance/view/creditwriteoff/add.html
  8. 25 14
      app/finance/view/creditwriteoff/datalist.html
  9. 25 14
      app/finance/view/priceadjust/datalist.html
  10. 126 0
      app/finance/view/vccpayment/add.html
  11. 119 0
      app/finance/view/vccpayment/datalist.html
  12. 67 0
      app/finance/view/vccpayment/view.html
  13. 141 0
      app/finance/view/vccpaymentcate/datalist.html
  14. 79 0
      app/home/controller/Currency.php
  15. 77 0
      app/home/model/Currency.php
  16. 134 0
      app/home/view/currency/datalist.html
  17. 1 1
      app/user/controller/User.php
  18. 2 2
      database/migrations/20260525061909_seed_price_adjust_data.php
  19. 2 2
      database/migrations/20260525070002_seed_credit_writeoff_data.php
  20. 27 0
      database/migrations/20260525090001_create_currency.php
  21. 69 0
      database/migrations/20260525090002_seed_currency_data.php
  22. 87 0
      database/migrations/20260525090003_seed_currency_rule.php
  23. 44 0
      database/migrations/20260525090004_create_vcc_payment.php
  24. 151 0
      database/migrations/20260525090005_seed_vcc_payment_data.php
  25. 26 0
      database/migrations/20260525090006_create_vcc_payment_cate.php
  26. 37 0
      database/migrations/20260525090007_seed_vcc_payment_cate_data.php
  27. 87 0
      database/migrations/20260525090008_seed_vcc_payment_cate_rule.php
  28. 3 3
      extend/avatars/MDAvatars.php

+ 1 - 1
app/api/controller/Import.php

@@ -45,7 +45,7 @@ class Import extends BaseController
         $Avatar = new MDAvatars($Char, 256, 1);
         $avatar_name = '/avatars/avatar_256_' . set_salt(10) . time() . '.png';
         $path = get_config('filesystem.disks.public.url') . $avatar_name;
-        $res = $Avatar->Save('.' . $path, 256);
+        $res = $Avatar->Save(public_path() . ltrim($path, '/'), 256);
         $Avatar->Free();
         return $path;
     }

+ 132 - 0
app/finance/controller/VccPayment.php

@@ -0,0 +1,132 @@
+<?php
+declare(strict_types=1);
+
+namespace app\finance\controller;
+
+use app\base\BaseController;
+use app\finance\model\VccPayment as VccPaymentModel;
+use app\finance\model\VccPaymentCate;
+use app\finance\validate\VccPaymentValidate;
+use think\exception\ValidateException;
+use think\facade\Db;
+use think\facade\View;
+
+class VccPayment extends BaseController
+{
+    protected $model;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->model = new VccPaymentModel();
+    }
+
+    public function datalist()
+    {
+        $param = get_params();
+        if (request()->isAjax()) {
+            $uid      = $this->uid;
+            $where    = [];
+            $whereOr  = [];
+            $where[]  = ['delete_time', '=', 0];
+
+            $tab = isset($param['tab']) ? (int) $param['tab'] : 0;
+            if ($tab === 0) {
+                $whereOr[] = ['admin_id', '=', $uid];
+                $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")];
+                $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_history_uids)")];
+                $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_copy_uids)")];
+            } elseif ($tab === 1) {
+                $where[] = ['admin_id', '=', $uid];
+            } elseif ($tab === 2) {
+                $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")];
+            } elseif ($tab === 3) {
+                $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_history_uids)")];
+            } elseif ($tab === 4) {
+                $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_copy_uids)")];
+            }
+
+            if (!empty($param['keywords'])) {
+                $where[] = ['payment_reason', 'like', '%' . $param['keywords'] . '%'];
+            }
+            if (isset($param['check_status']) && $param['check_status'] !== '') {
+                $where[] = ['check_status', '=', $param['check_status']];
+            }
+            if (!empty($param['diff_time'])) {
+                $diff = explode('~', $param['diff_time']);
+                $where[] = ['create_time', 'between', [strtotime(urldecode($diff[0])), strtotime(urldecode($diff[1] . ' 23:59:59'))]];
+            }
+
+            $list = $this->model->datalist($param, $where, $whereOr);
+            return table_assign(0, '', $list);
+        }
+        return view();
+    }
+
+    public function add()
+    {
+        $param = get_params();
+        if (request()->isAjax()) {
+            if (!empty($param['id']) && $param['id'] > 0) {
+                try {
+                    validate(VccPaymentValidate::class)->scene('edit')->check($param);
+                } catch (ValidateException $e) {
+                    return to_assign(1, $e->getError());
+                }
+                $this->model->edit($param);
+            } else {
+                try {
+                    validate(VccPaymentValidate::class)->scene('add')->check($param);
+                } catch (ValidateException $e) {
+                    return to_assign(1, $e->getError());
+                }
+                $param['admin_id'] = $this->uid;
+                $param['did']      = $this->did;
+                $this->model->add($param);
+            }
+        } else {
+            $id = isset($param['id']) ? (int) $param['id'] : 0;
+            $detail = [
+                'id'             => 0,
+                'payment_reason' => '',
+                'amount'         => '',
+                'currency'       => '',
+                'payment_method' => 0,
+                'payment_date'   => '',
+                'bank_account'   => '',
+                'remark'         => '',
+                'file_ids'       => '',
+                'check_status'   => 0,
+                'check_flow_id'  => 0,
+            ];
+            if ($id > 0) {
+                $detail = $this->model->getById($id);
+            }
+            View::assign('detail', $detail);
+            View::assign('currency_list', Db::name('Currency')->where(['status' => 1, 'delete_time' => 0])->order('sort asc, id asc')->select());
+            View::assign('payment_methods', VccPaymentCate::getByType(1));
+            return view();
+        }
+    }
+
+    public function view($id)
+    {
+        $detail = $this->model->getById($id);
+        if (!empty($detail)) {
+            View::assign('create_user', get_admin($detail['admin_id']));
+            View::assign('detail', $detail);
+            return view();
+        }
+        return view(EEEOR_REPORTING, ['code' => 404, 'warning' => '找不到页面']);
+    }
+
+    public function del()
+    {
+        $param = get_params();
+        $id    = isset($param['id']) ? (int) $param['id'] : 0;
+        if (request()->isDelete()) {
+            return $this->model->delById($id);
+        }
+        return to_assign(1, '错误的请求');
+    }
+}

+ 76 - 0
app/finance/controller/VccPaymentCate.php

@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+
+namespace app\finance\controller;
+
+use app\base\BaseController;
+use app\finance\model\VccPaymentCate as VccPaymentCateModel;
+use think\facade\View;
+
+class VccPaymentCate extends BaseController
+{
+    protected $model;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->model = new VccPaymentCateModel();
+    }
+
+    public function datalist()
+    {
+        if (request()->isAjax()) {
+            $param = get_params();
+            $type  = isset($param['type']) ? (int) $param['type'] : 0;
+            $where = [['delete_time', '=', 0]];
+            if ($type > 0) {
+                $where[] = ['type', '=', $type];
+            }
+            $list = $this->model->where($where)->order('sort asc, id asc')->select();
+            return to_assign(0, '', $list);
+        }
+        return view();
+    }
+
+    public function add()
+    {
+        $param = get_params();
+        if (request()->isAjax()) {
+            if (!empty($param['id']) && $param['id'] > 0) {
+                $this->model->edit($param);
+            } else {
+                $this->model->add($param);
+            }
+        } else {
+            $id = isset($param['id']) ? (int) $param['id'] : 0;
+            if ($id > 0) {
+                View::assign('detail', $this->model->getById($id));
+            }
+            return view();
+        }
+    }
+
+    public function set()
+    {
+        if (request()->isAjax()) {
+            $param = get_params();
+            $res   = $this->model->strict(false)->field('id,status')->update($param);
+            if ($res) {
+                add_log($param['status'] == 1 ? 'recovery' : 'disable', $param['id'], $param);
+                return to_assign();
+            }
+            return to_assign(0, '操作失败');
+        }
+        return to_assign(1, '错误的请求');
+    }
+
+    public function del()
+    {
+        if (request()->isDelete()) {
+            $param = get_params();
+            $id    = isset($param['id']) ? (int) $param['id'] : 0;
+            return $this->model->delById($id);
+        }
+        return to_assign(1, '错误的请求');
+    }
+}

+ 90 - 0
app/finance/model/VccPayment.php

@@ -0,0 +1,90 @@
+<?php
+namespace app\finance\model;
+
+use think\Model;
+use think\facade\Db;
+
+class VccPayment extends Model
+{
+    public function datalist($param = [], $where = [], $whereOr = [])
+    {
+        $rows  = empty($param['limit']) ? get_config('app.page_size') : $param['limit'];
+        $order = empty($param['order']) ? 'id desc' : $param['order'];
+        try {
+            $methodMap = Db::name('VccPaymentCate')->where(['type' => 1, 'delete_time' => 0])->column('title', 'value');
+            $list = self::where($where)
+                ->where(function ($query) use ($whereOr) {
+                    if (!empty($whereOr)) {
+                        $query->whereOr($whereOr);
+                    }
+                })
+                ->order($order)
+                ->paginate(['list_rows' => $rows])
+                ->each(function ($item) use ($methodMap) {
+                    $item->check_status_str     = check_status_name($item->check_status);
+                    $item->payment_method_str   = $methodMap[$item->payment_method] ?? '';
+                    $item->admin_name           = Db::name('Admin')->where('id', $item->admin_id)->value('name');
+                    $item->department           = Db::name('Department')->where('id', $item->did)->value('title');
+                    $item->create_time_str      = to_date($item->create_time);
+                });
+            return $list;
+        } catch (\Exception $e) {
+            return ['code' => 1, 'data' => [], 'msg' => $e->getMessage()];
+        }
+    }
+
+    public function add($param)
+    {
+        try {
+            $param['create_time'] = time();
+            $insertId = self::strict(false)->field(true)->insertGetId($param);
+            add_log('add', $insertId, $param, 'VCC返点/提成付款申请');
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign(0, '操作成功', ['return_id' => $insertId]);
+    }
+
+    public function edit($param)
+    {
+        try {
+            $param['update_time'] = time();
+            self::where('id', $param['id'])->strict(false)->field(true)->update($param);
+            add_log('edit', $param['id'], $param, 'VCC返点/提成付款申请');
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign(0, '操作成功', ['return_id' => $param['id']]);
+    }
+
+    public function getById($id)
+    {
+        $info = self::find($id);
+        if (empty($info)) {
+            return [];
+        }
+        $info['admin_name']          = Db::name('Admin')->where('id', $info['admin_id'])->value('name');
+        $info['department']          = Db::name('Department')->where('id', $info['did'])->value('title');
+        $info['payment_method_name'] = Db::name('VccPaymentCate')->where(['type' => 1, 'value' => $info['payment_method'], 'delete_time' => 0])->value('title') ?? '';
+        $info['currency_name']       = Db::name('Currency')->where(['code' => $info['currency'], 'delete_time' => 0])->value('title') ?? '';
+        if (!empty($info['file_ids'])) {
+            $info['file_array'] = Db::name('File')->where('id', 'in', $info['file_ids'])->select();
+        }
+        return $info;
+    }
+
+    public function delById($id, $type = 0)
+    {
+        try {
+            if ($type == 0) {
+                self::where('id', $id)->update(['delete_time' => time()]);
+            } else {
+                self::destroy($id);
+            }
+            add_log('delete', $id);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign();
+    }
+}

+ 71 - 0
app/finance/model/VccPaymentCate.php

@@ -0,0 +1,71 @@
+<?php
+namespace app\finance\model;
+
+use think\Model;
+
+class VccPaymentCate extends Model
+{
+    public function add($param)
+    {
+        if ($this->valueExists((int) $param['type'], (int) $param['value'])) {
+            return to_assign(1, '该类型下「值」已存在,请使用其它值');
+        }
+        try {
+            $param['create_time'] = time();
+            $insertId = self::strict(false)->field(true)->insertGetId($param);
+            add_log('add', $insertId, $param);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign(0, '操作成功', ['return_id' => $insertId]);
+    }
+
+    public function edit($param)
+    {
+        if ($this->valueExists((int) $param['type'], (int) $param['value'], (int) $param['id'])) {
+            return to_assign(1, '该类型下「值」已存在,请使用其它值');
+        }
+        try {
+            self::where('id', $param['id'])->strict(false)->field(true)->update($param);
+            add_log('edit', $param['id'], $param);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign(0, '操作成功', ['return_id' => $param['id']]);
+    }
+
+    /**
+     * 校验同 type 下 value 是否已被占用(编辑时排除自身)
+     */
+    protected function valueExists(int $type, int $value, int $excludeId = 0): bool
+    {
+        $query = self::where(['type' => $type, 'value' => $value, 'delete_time' => 0]);
+        if ($excludeId > 0) {
+            $query->where('id', '<>', $excludeId);
+        }
+        return $query->count() > 0;
+    }
+
+    public function getById($id)
+    {
+        return self::find($id);
+    }
+
+    public function delById($id)
+    {
+        try {
+            self::where('id', $id)->update(['delete_time' => time()]);
+            add_log('delete', $id);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign();
+    }
+
+    public static function getByType($type)
+    {
+        return self::where(['type' => $type, 'status' => 1, 'delete_time' => 0])
+            ->order('sort asc, id asc')
+            ->select();
+    }
+}

+ 31 - 0
app/finance/validate/VccPaymentValidate.php

@@ -0,0 +1,31 @@
+<?php
+namespace app\finance\validate;
+
+use think\Validate;
+
+class VccPaymentValidate extends Validate
+{
+    protected $rule = [
+        'payment_reason' => 'require|max:500',
+        'amount'         => 'require|float|gt:0',
+        'currency'       => 'require|max:8',
+        'payment_method' => 'require',
+        'payment_date'   => 'require|date',
+    ];
+
+    protected $message = [
+        'payment_reason.require' => '付款事由不能为空',
+        'amount.require'         => '金额不能为空',
+        'amount.float'           => '金额必须为数字',
+        'amount.gt'              => '金额必须大于0',
+        'currency.require'       => '请选择金额单位(币种)',
+        'payment_method.require' => '请选择付款方式',
+        'payment_date.require'   => '请选择付款日期',
+        'payment_date.date'      => '付款日期格式不正确',
+    ];
+
+    protected $scene = [
+        'add'  => ['payment_reason', 'amount', 'currency', 'payment_method', 'payment_date'],
+        'edit' => ['payment_reason', 'amount', 'currency', 'payment_method', 'payment_date'],
+    ];
+}

+ 2 - 8
app/finance/view/creditwriteoff/add.html

@@ -42,7 +42,7 @@
         <tr>
             <td class="layui-td-gray-2">归还日期<font>*</font></td>
             <td colspan="3">
-                <input type="text" name="return_date" id="return_date" class="layui-input" readonly
+                <input type="text" name="return_date" id="return_date" class="layui-input tool-time" readonly
                        value="{$detail.return_date|default=''}"
                        lay-verify="required" lay-reqText="请选择归还日期"
                        placeholder="请选择归还日期">
@@ -88,18 +88,12 @@
 <script>
     const moduleInit = ['tool', 'uploadPlus', 'oaCheck'];
     function gouguInit() {
-        var form = layui.form, tool = layui.tool, laydate = layui.laydate,
+        var form = layui.form, tool = layui.tool,
             uploadPlus = layui.uploadPlus, oaCheck = layui.oaCheck;
 
         oaCheck.init({ check_name: 'credit_writeoff', check_btn: 0 });
         var fileUp = new uploadPlus();
 
-        laydate.render({
-            elem: '#return_date',
-            type: 'date',
-            trigger: 'click'
-        });
-
         form.on('submit(webform)', function (data) {
             let callback = function (e) {
                 layer.msg(e.msg);

+ 25 - 14
app/finance/view/creditwriteoff/datalist.html

@@ -43,9 +43,9 @@
 
 {block name="script"}
 <script>
-    const moduleInit = ['tool', 'oaTable'];
+    const moduleInit = ['tool', 'tablePlus'];
     function gouguInit() {
-        var tool = layui.tool, table = layui.table, form = layui.form;
+        var tool = layui.tool, table = layui.tablePlus, form = layui.form;
 
         var tab = 0;
         var tableIns = table.render({
@@ -65,22 +65,33 @@
                 {
                     title: '操作', width: 160, align: 'center', fixed: 'right',
                     templet: function (d) {
-                        return '<a class="layui-btn layui-btn-xs" data-href="/finance/creditwriteoff/view/' + d.id + '" data-title="授信&核销申请详情" data-width="800" data-height="90%">查看</a>'
-                            + '<a class="layui-btn layui-btn-xs layui-btn-danger j-del" data-id="' + d.id + '">删除</a>';
+                        var html = '<div class="layui-btn-group">';
+                        html += '<span class="layui-btn layui-btn-normal layui-btn-xs" lay-event="view">查看</span>';
+                        html += '<span class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</span>';
+                        html += '</div>';
+                        return html;
                     }
                 }
-            ]],
-            done: function () {
-                tool.sideOpen();
-                // 删除
-                $('body').off('click', '.j-del').on('click', '.j-del', function () {
-                    var id = $(this).data('id');
-                    layer.confirm('确认删除该授信&核销申请?', { icon: 3 }, function () {
-                        tool.delete('/finance/creditwriteoff/del', { id: id }, function () {
-                            tableIns.reload();
-                        });
+            ]]
+        });
+
+        table.on('tool(table_creditwriteoff)', function (obj) {
+            var data = obj.data;
+            if (obj.event === 'view') {
+                tool.side('/finance/creditwriteoff/view?id=' + data.id);
+                return;
+            }
+            if (obj.event === 'del') {
+                layer.confirm('确认删除该授信&核销申请?', { icon: 3, title: '提示' }, function (index) {
+                    tool.delete('/finance/creditwriteoff/del', { id: data.id }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) {
+                            obj.del();
+                        }
                     });
+                    layer.close(index);
                 });
+                return;
             }
         });
 

+ 25 - 14
app/finance/view/priceadjust/datalist.html

@@ -43,9 +43,9 @@
 
 {block name="script"}
 <script>
-    const moduleInit = ['tool', 'oaTable'];
+    const moduleInit = ['tool', 'tablePlus'];
     function gouguInit() {
-        var tool = layui.tool, table = layui.table, form = layui.form;
+        var tool = layui.tool, table = layui.tablePlus, form = layui.form;
 
         var tab = 0;
         var tableIns = table.render({
@@ -63,22 +63,33 @@
                 {
                     title: '操作', width: 160, align: 'center', fixed: 'right',
                     templet: function (d) {
-                        return '<a class="layui-btn layui-btn-xs" data-href="/finance/priceadjust/view/' + d.id + '" data-title="调价申请详情" data-width="800" data-height="90%">查看</a>'
-                            + '<a class="layui-btn layui-btn-xs layui-btn-danger j-del" data-id="' + d.id + '">删除</a>';
+                        var html = '<div class="layui-btn-group">';
+                        html += '<span class="layui-btn layui-btn-normal layui-btn-xs" lay-event="view">查看</span>';
+                        html += '<span class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</span>';
+                        html += '</div>';
+                        return html;
                     }
                 }
-            ]],
-            done: function () {
-                tool.sideOpen();
-                // 删除
-                $('body').off('click', '.j-del').on('click', '.j-del', function () {
-                    var id = $(this).data('id');
-                    layer.confirm('确认删除该调价申请?', { icon: 3 }, function () {
-                        tool.delete('/finance/priceadjust/del', { id: id }, function () {
-                            tableIns.reload();
-                        });
+            ]]
+        });
+
+        table.on('tool(table_priceadjust)', function (obj) {
+            var data = obj.data;
+            if (obj.event === 'view') {
+                tool.side('/finance/priceadjust/view?id=' + data.id);
+                return;
+            }
+            if (obj.event === 'del') {
+                layer.confirm('确认删除该调价申请?', { icon: 3, title: '提示' }, function (index) {
+                    tool.delete('/finance/priceadjust/del', { id: data.id }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) {
+                            obj.del();
+                        }
                     });
+                    layer.close(index);
                 });
+                return;
             }
         });
 

+ 126 - 0
app/finance/view/vccpayment/add.html

@@ -0,0 +1,126 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<form class="layui-form p-page" lay-filter="form-vccpayment">
+    <h3 class="pb-2">VCC返点/提成付款申请</h3>
+    <table class="layui-table layui-table-form">
+        <tr>
+            <td class="layui-td-gray-2">付款事由<font>*</font></td>
+            <td colspan="3">
+                <input type="text" name="payment_reason" class="layui-input"
+                       value="{$detail.payment_reason|default=''}"
+                       lay-verify="required" lay-reqText="请输入付款事由"
+                       placeholder="请输入付款事由">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">金额<font>*</font></td>
+            <td>
+                <input type="text" name="amount" class="layui-input"
+                       value="{$detail.amount|default=''}"
+                       lay-verify="required" lay-reqText="请输入金额"
+                       placeholder="请输入金额">
+            </td>
+            <td class="layui-td-gray">金额单位<font>*</font></td>
+            <td>
+                <select name="currency" lay-verify="required" lay-reqText="请选择金额单位" lay-search>
+                    <option value="">-- 请选择币种 --</option>
+                    {volist name="$currency_list" id="vo"}
+                    <option value="{$vo.code}" {eq name="$detail.currency" value="$vo.code"} selected{/eq}>{$vo.code} {$vo.title}</option>
+                    {/volist}
+                </select>
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">付款方式<font>*</font></td>
+            <td colspan="3">
+                <select name="payment_method" lay-verify="required" lay-reqText="请选择付款方式">
+                    <option value="">-- 请选择 --</option>
+                    {volist name="$payment_methods" id="vo"}
+                    <option value="{$vo.value}" {eq name="$detail.payment_method" value="$vo.value"} selected{/eq}>{$vo.title}</option>
+                    {/volist}
+                </select>
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">付款日期<font>*</font></td>
+            <td colspan="3">
+                <input type="text" name="payment_date" id="payment_date" class="layui-input tool-time" readonly
+                       value="{$detail.payment_date|default=''}"
+                       lay-verify="required" lay-reqText="请选择付款日期"
+                       placeholder="请选择付款日期">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">银行账户</td>
+            <td colspan="3">
+                <input type="text" name="bank_account" class="layui-input"
+                       value="{$detail.bank_account|default=''}"
+                       placeholder="选填">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">备注</td>
+            <td colspan="3">
+                <textarea name="remark" class="layui-textarea"
+                          placeholder="选填">{$detail.remark|default=''}</textarea>
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">附件 <button type="button" class="layui-btn layui-btn-xs" id="uploadBtn"><i class="layui-icon layui-icon-upload"></i></button></td>
+            <td colspan="3">
+                <div class="layui-row" id="uploadBox">
+                    <input data-type="file" type="hidden" name="file_ids" value="{$detail.file_ids|default=''}">
+                    {notempty name="$detail.file_ids"}
+                    {volist name="$detail.file_array" id="vo"}
+                    <div class="layui-col-md4">{:file_card($vo)}</div>
+                    {/volist}
+                    {/notempty}
+                </div>
+            </td>
+        </tr>
+    </table>
+
+    <div id="checkBox"
+         data-status="{$detail.check_status|default=0}"
+         data-id="{$detail.id|default=0}"
+         data-checkflowid="{$detail.check_flow_id|default=0}"
+         class="pt-3"></div>
+
+    <div class="pt-4">
+        <input type="hidden" name="id" value="{$detail.id|default=0}">
+        <button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="webform">立即提交</button>
+        <button type="reset" class="layui-btn layui-btn-primary">重置</button>
+    </div>
+</form>
+{/block}
+
+{block name="script"}
+<script>
+    const moduleInit = ['tool', 'uploadPlus', 'oaCheck'];
+    function gouguInit() {
+        var form = layui.form, tool = layui.tool,
+            uploadPlus = layui.uploadPlus, oaCheck = layui.oaCheck;
+
+        oaCheck.init({ check_name: 'vcc_payment', check_btn: 0 });
+        var fileUp = new uploadPlus();
+
+        form.on('submit(webform)', function (data) {
+            let callback = function (e) {
+                layer.msg(e.msg);
+                if (e.code == 0) {
+                    let checkCallback = function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) tool.sideClose(1000);
+                    };
+                    data.field.check_name = 'vcc_payment';
+                    data.field.action_id  = e.data.return_id;
+                    oaCheck.submit(data.field, checkCallback);
+                }
+            };
+            let clickbtn = $(this);
+            tool.post('/finance/vccpayment/add', data.field, callback, clickbtn);
+            return false;
+        });
+    }
+</script>
+{/block}

+ 119 - 0
app/finance/view/vccpayment/datalist.html

@@ -0,0 +1,119 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<div class="p-page">
+    <div class="layui-card">
+        <div class="layui-card-header">
+            <div class="layui-form" lay-filter="form-search">
+                <div class="layui-inline">
+                    <input class="layui-input" name="keywords" id="keywords" placeholder="搜索付款事由">
+                </div>
+                <div class="layui-inline">
+                    <select name="check_status" id="check_status">
+                        <option value="">全部状态</option>
+                        <option value="0">待审核</option>
+                        <option value="1">审核中</option>
+                        <option value="2">已通过</option>
+                        <option value="3">已驳回</option>
+                        <option value="4">已撤销</option>
+                    </select>
+                </div>
+                <div class="layui-inline">
+                    <button class="layui-btn layui-btn-sm" id="btnSearch">搜索</button>
+                    <button type="button" class="layui-btn layui-btn-sm layui-btn-normal tool-add"
+                            data-href="/finance/vccpayment/add">新建VCC返点/提成付款申请
+                    </button>
+                </div>
+            </div>
+        </div>
+        <div class="layui-card-body">
+            <div class="layui-tab layui-tab-brief">
+                <ul class="layui-tab-title">
+                    <li class="layui-this" data-tab="0">全部</li>
+                    <li data-tab="1">我创建的</li>
+                    <li data-tab="2">待我审核</li>
+                    <li data-tab="3">我已审核</li>
+                    <li data-tab="4">抄送给我</li>
+                </ul>
+            </div>
+            <table id="table_vccpayment" lay-filter="table_vccpayment"></table>
+        </div>
+    </div>
+</div>
+{/block}
+
+{block name="script"}
+<script>
+    const moduleInit = ['tool', 'tablePlus'];
+    function gouguInit() {
+        var tool = layui.tool, table = layui.tablePlus, form = layui.form;
+
+        var tab = 0;
+        var tableIns = table.render({
+            elem: '#table_vccpayment',
+            url: '/finance/vccpayment/datalist',
+            where: { tab: tab },
+            page: true,
+            cols: [[
+                { field: 'id', title: 'ID', width: 80 },
+                { field: 'payment_reason', title: '付款事由', minWidth: 160 },
+                { field: 'amount', title: '金额', width: 120 },
+                { field: 'currency', title: '币种', width: 90 },
+                { field: 'payment_method_str', title: '付款方式', width: 100 },
+                { field: 'payment_date', title: '付款日期', width: 120 },
+                { field: 'admin_name', title: '申请人', width: 100 },
+                { field: 'department', title: '部门', width: 120 },
+                { field: 'check_status_str', title: '审批状态', width: 100 },
+                { field: 'create_time_str', title: '申请时间', width: 160 },
+                {
+                    title: '操作', width: 160, align: 'center', fixed: 'right',
+                    templet: function (d) {
+                        var html = '<div class="layui-btn-group">';
+                        html += '<span class="layui-btn layui-btn-normal layui-btn-xs" lay-event="view">查看</span>';
+                        html += '<span class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</span>';
+                        html += '</div>';
+                        return html;
+                    }
+                }
+            ]]
+        });
+
+        table.on('tool(table_vccpayment)', function (obj) {
+            var data = obj.data;
+            if (obj.event === 'view') {
+                tool.side('/finance/vccpayment/view?id=' + data.id);
+                return;
+            }
+            if (obj.event === 'del') {
+                layer.confirm('确认删除该付款申请?', { icon: 3, title: '提示' }, function (index) {
+                    tool.delete('/finance/vccpayment/del', { id: data.id }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) {
+                            obj.del();
+                        }
+                    });
+                    layer.close(index);
+                });
+                return;
+            }
+        });
+
+        // Tab 切换
+        $('body').on('click', '.layui-tab-title li', function () {
+            tab = $(this).data('tab');
+            tableIns.reload({ where: { tab: tab }, page: { curr: 1 } });
+        });
+
+        // 搜索
+        $('#btnSearch').on('click', function () {
+            tableIns.reload({
+                where: {
+                    tab: tab,
+                    keywords: $('#keywords').val(),
+                    check_status: $('#check_status').val()
+                },
+                page: { curr: 1 }
+            });
+        });
+    }
+</script>
+{/block}

+ 67 - 0
app/finance/view/vccpayment/view.html

@@ -0,0 +1,67 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<div class="p-page">
+    <h3 class="pb-2">VCC返点/提成付款申请详情</h3>
+    <table class="layui-table layui-table-form">
+        <tr>
+            <td class="layui-td-gray-2">申请人</td>
+            <td>{$create_user.name|default=''} ({$create_user.department|default=''})</td>
+            <td class="layui-td-gray">申请时间</td>
+            <td>{:to_date($detail['create_time'])}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">付款事由</td>
+            <td colspan="3">{$detail.payment_reason|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">金额</td>
+            <td>{$detail.amount|default=''} {$detail.currency|default=''}</td>
+            <td class="layui-td-gray">金额单位</td>
+            <td>{$detail.currency|default=''}({$detail.currency_name|default=''})</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">付款方式</td>
+            <td>{$detail.payment_method_name|default=''}</td>
+            <td class="layui-td-gray">付款日期</td>
+            <td>{$detail.payment_date|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">银行账户</td>
+            <td colspan="3">{$detail.bank_account|default='-'}</td>
+        </tr>
+        {notempty name="$detail.remark"}
+        <tr>
+            <td class="layui-td-gray-2">备注</td>
+            <td colspan="3">{$detail.remark}</td>
+        </tr>
+        {/notempty}
+        {notempty name="$detail.file_ids"}
+        <tr>
+            <td class="layui-td-gray-2">相关附件</td>
+            <td colspan="3">
+                <div class="layui-row">
+                    {volist name="$detail.file_array" id="vo"}
+                    <div class="layui-col-md4">{:file_card($vo,'view')}</div>
+                    {/volist}
+                </div>
+            </td>
+        </tr>
+        {/notempty}
+    </table>
+    <div id="checkBox"
+         data-status="{$detail.check_status}"
+         data-id="{$detail.id}"
+         data-checkflowid="{$detail.check_flow_id}"
+         class="pt-3"></div>
+</div>
+{/block}
+
+{block name="script"}
+<script>
+    var moduleInit = ['tool', 'oaCheck'];
+    function gouguInit() {
+        var oaCheck = layui.oaCheck;
+        oaCheck.init({ check_name: 'vcc_payment' });
+    }
+</script>
+{/block}

+ 141 - 0
app/finance/view/vccpaymentcate/datalist.html

@@ -0,0 +1,141 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<div class="p-page">
+    <div class="layui-tab layui-tab-brief">
+        <ul class="layui-tab-title">
+            <li class="layui-this" data-type="1">付款方式</li>
+        </ul>
+    </div>
+    <table class="layui-hide" id="table_vcc_payment_cate" lay-filter="table_vcc_payment_cate"></table>
+</div>
+
+<script type="text/html" id="toolbarDemo">
+    <div class="layui-btn-container">
+        <button class="layui-btn layui-btn-sm add-new" type="button">+ 添加</button>
+    </div>
+</script>
+{/block}
+
+{block name="script"}
+<script>
+    const moduleInit = ['tool'];
+    var currentType = 1;
+
+    function gouguInit() {
+        var table = layui.table, tool = layui.tool;
+
+        layui.pageTable = table.render({
+            elem: '#table_vcc_payment_cate',
+            toolbar: '#toolbarDemo',
+            url: '/finance/vccpaymentcate/datalist',
+            where: { type: currentType },
+            page: false,
+            limit: 999,
+            cellMinWidth: 80,
+            cols: [[
+                { field: 'id', width: 80, title: 'ID', align: 'center' },
+                { field: 'title', title: '名称' },
+                { field: 'value', width: 80, title: '值', align: 'center' },
+                { field: 'is_other', width: 90, title: '其他项', align: 'center', templet: function (d) {
+                    return d.is_other == 1 ? '<span class="layui-badge layui-bg-blue">是</span>' : '-';
+                }},
+                { field: 'sort', width: 80, title: '排序', align: 'center' },
+                { field: 'status', title: '状态', width: 80, align: 'center', templet: function (d) {
+                    return d.status == 1 ? '<span class="green">启用</span>' : '<span class="yellow">禁用</span>';
+                }},
+                { width: 160, title: '操作', align: 'center', templet: function (d) {
+                    var edit = '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit">编辑</a>';
+                    var disable = '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="disable">禁用</a>';
+                    var enable = '<a class="layui-btn layui-btn-xs" lay-event="open">启用</a>';
+                    var del = '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>';
+                    var toggle = d.status == 1 ? disable : enable;
+                    return '<div class="layui-btn-group">' + edit + toggle + del + '</div>';
+                }}
+            ]]
+        });
+
+        table.on('tool(table_vcc_payment_cate)', function (obj) {
+            if (obj.event === 'edit') {
+                editCate(obj.data);
+            }
+            if (obj.event === 'disable') {
+                layer.confirm('确定要禁用该记录吗?', { icon: 3 }, function (index) {
+                    tool.post('/finance/vccpaymentcate/set', { id: obj.data.id, status: 0 }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) layui.pageTable.reload();
+                    });
+                    layer.close(index);
+                });
+            }
+            if (obj.event === 'open') {
+                layer.confirm('确定要启用该记录吗?', { icon: 3 }, function (index) {
+                    tool.post('/finance/vccpaymentcate/set', { id: obj.data.id, status: 1 }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) layui.pageTable.reload();
+                    });
+                    layer.close(index);
+                });
+            }
+            if (obj.event === 'del') {
+                layer.confirm('确定要删除该记录吗?', { icon: 3 }, function (index) {
+                    tool.delete('/finance/vccpaymentcate/del', { id: obj.data.id }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) layui.pageTable.reload();
+                    });
+                    layer.close(index);
+                });
+            }
+        });
+
+        // Tab 切换
+        $('body').on('click', '.layui-tab-title li', function () {
+            currentType = $(this).data('type');
+            layui.pageTable.reload({ where: { type: currentType }, page: { curr: 1 } });
+        });
+
+        $('body').on('click', '.add-new', function () {
+            editCate();
+        });
+
+        function editCate(row) {
+            row = row || {};
+            var isEdit  = row.id > 0;
+            var id      = row.id || 0;
+            var title   = row.title || '';
+            var value   = (row.value === undefined || row.value === null) ? '' : row.value;
+            var sort    = row.sort || 0;
+            var isOther = row.is_other == 1;
+            layer.open({
+                type: 1,
+                title: isEdit ? '编辑' : '添加',
+                area: ['360px', 'auto'],
+                content: '<div style="padding:20px">'
+                    + '<div class="layui-form-item"><label>名称</label>'
+                    + '<input id="cate_title" class="layui-input" value="' + title + '" placeholder="请输入名称"></div>'
+                    + '<div class="layui-form-item" style="margin-top:10px"><label>值(同类型内唯一)</label>'
+                    + '<input id="cate_value" class="layui-input" type="number" value="' + value + '" placeholder="请输入数字值"></div>'
+                    + '<div class="layui-form-item" style="margin-top:10px"><label>排序</label>'
+                    + '<input id="cate_sort" class="layui-input" type="number" value="' + sort + '"></div>'
+                    + '<div class="layui-form-item" style="margin-top:10px"><label><input id="cate_is_other" type="checkbox"' + (isOther ? ' checked' : '') + '> 标记为「其他」项(选中后申请时需填备注)</label></div>'
+                    + '</div>',
+                btn: ['确定', '取消'],
+                yes: function (index) {
+                    var val   = $('#cate_title').val();
+                    var vval  = $('#cate_value').val();
+                    var sval  = $('#cate_sort').val();
+                    var other = $('#cate_is_other').prop('checked') ? 1 : 0;
+                    if (!val) { layer.msg('请填写名称'); return; }
+                    if (vval === '' || isNaN(vval)) { layer.msg('请填写数字值'); return; }
+                    tool.post('/finance/vccpaymentcate/add',
+                        { id: id, type: currentType, title: val, value: vval, sort: sval, is_other: other },
+                        function (e) {
+                            layer.msg(e.msg);
+                            if (e.code == 0) { layer.close(index); layui.pageTable.reload(); }
+                        }
+                    );
+                }
+            });
+        }
+    }
+</script>
+{/block}

+ 79 - 0
app/home/controller/Currency.php

@@ -0,0 +1,79 @@
+<?php
+declare(strict_types=1);
+
+namespace app\home\controller;
+
+use app\base\BaseController;
+use app\home\model\Currency as CurrencyModel;
+use think\facade\View;
+
+class Currency extends BaseController
+{
+    protected $model;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->model = new CurrencyModel();
+    }
+
+    public function datalist()
+    {
+        if (request()->isAjax()) {
+            $param = get_params();
+            $where = [['delete_time', '=', 0]];
+            if (!empty($param['keywords'])) {
+                $where[] = ['code|title', 'like', '%' . $param['keywords'] . '%'];
+            }
+            $list = $this->model->where($where)->order('sort asc, id asc')->select();
+            return to_assign(0, '', $list);
+        }
+        return view();
+    }
+
+    public function add()
+    {
+        $param = get_params();
+        if (request()->isAjax()) {
+            if (empty($param['code'])) {
+                return to_assign(1, '币种代码不能为空');
+            }
+            if (empty($param['title'])) {
+                return to_assign(1, '币种名称不能为空');
+            }
+            if (!empty($param['id']) && $param['id'] > 0) {
+                return $this->model->edit($param);
+            }
+            return $this->model->add($param);
+        }
+        $id = isset($param['id']) ? (int) $param['id'] : 0;
+        if ($id > 0) {
+            View::assign('detail', $this->model->getById($id));
+        }
+        return view();
+    }
+
+    public function set()
+    {
+        if (request()->isAjax()) {
+            $param = get_params();
+            $res   = $this->model->strict(false)->field('id,status')->update($param);
+            if ($res) {
+                add_log($param['status'] == 1 ? 'recovery' : 'disable', $param['id'], $param);
+                return to_assign();
+            }
+            return to_assign(0, '操作失败');
+        }
+        return to_assign(1, '错误的请求');
+    }
+
+    public function del()
+    {
+        if (request()->isDelete()) {
+            $param = get_params();
+            $id    = isset($param['id']) ? (int) $param['id'] : 0;
+            return $this->model->delById($id);
+        }
+        return to_assign(1, '错误的请求');
+    }
+}

+ 77 - 0
app/home/model/Currency.php

@@ -0,0 +1,77 @@
+<?php
+namespace app\home\model;
+
+use think\Model;
+
+class Currency extends Model
+{
+    public function add($param)
+    {
+        if ($this->codeExists((string) $param['code'])) {
+            return to_assign(1, '该币种代码已存在,请勿重复添加');
+        }
+        try {
+            $param['code']        = strtoupper(trim((string) $param['code']));
+            $param['create_time'] = time();
+            $insertId = self::strict(false)->field(true)->insertGetId($param);
+            add_log('add', $insertId, $param);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign(0, '操作成功', ['return_id' => $insertId]);
+    }
+
+    public function edit($param)
+    {
+        if ($this->codeExists((string) $param['code'], (int) $param['id'])) {
+            return to_assign(1, '该币种代码已存在,请勿重复添加');
+        }
+        try {
+            $param['code'] = strtoupper(trim((string) $param['code']));
+            self::where('id', $param['id'])->strict(false)->field(true)->update($param);
+            add_log('edit', $param['id'], $param);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign(0, '操作成功', ['return_id' => $param['id']]);
+    }
+
+    /**
+     * 校验币种代码是否已被占用(编辑时排除自身)
+     */
+    protected function codeExists(string $code, int $excludeId = 0): bool
+    {
+        $code  = strtoupper(trim($code));
+        $query = self::where(['code' => $code, 'delete_time' => 0]);
+        if ($excludeId > 0) {
+            $query->where('id', '<>', $excludeId);
+        }
+        return $query->count() > 0;
+    }
+
+    public function getById($id)
+    {
+        return self::find($id);
+    }
+
+    public function delById($id)
+    {
+        try {
+            self::where('id', $id)->update(['delete_time' => time()]);
+            add_log('delete', $id);
+        } catch (\Exception $e) {
+            return to_assign(1, '操作失败,原因:' . $e->getMessage());
+        }
+        return to_assign();
+    }
+
+    /**
+     * 业务表单下拉用:取所有启用币种,按 sort 排序
+     */
+    public static function getEnabled()
+    {
+        return self::where(['status' => 1, 'delete_time' => 0])
+            ->order('sort asc, id asc')
+            ->select();
+    }
+}

+ 134 - 0
app/home/view/currency/datalist.html

@@ -0,0 +1,134 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<div class="p-page">
+    <div class="layui-card">
+        <div class="layui-card-header">
+            <div class="layui-form" lay-filter="form-search">
+                <div class="layui-inline">
+                    <input class="layui-input" name="keywords" id="keywords" placeholder="搜索币种代码/名称">
+                </div>
+                <div class="layui-inline">
+                    <button class="layui-btn layui-btn-sm" id="btnSearch">搜索</button>
+                    <button type="button" class="layui-btn layui-btn-sm layui-btn-normal add-new">+ 添加币种</button>
+                </div>
+            </div>
+        </div>
+        <div class="layui-card-body">
+            <table class="layui-hide" id="table_currency" lay-filter="table_currency"></table>
+        </div>
+    </div>
+</div>
+{/block}
+
+{block name="script"}
+<script>
+    const moduleInit = ['tool'];
+
+    function gouguInit() {
+        var table = layui.table, tool = layui.tool;
+
+        layui.pageTable = table.render({
+            elem: '#table_currency',
+            url: '/home/currency/datalist',
+            page: false,
+            limit: 9999,
+            cellMinWidth: 80,
+            cols: [[
+                { field: 'id', width: 80, title: 'ID', align: 'center' },
+                { field: 'code', width: 120, title: '币种代码', align: 'center' },
+                { field: 'title', title: '币种名称' },
+                { field: 'sort', width: 90, title: '排序', align: 'center' },
+                { field: 'status', title: '状态', width: 90, align: 'center', templet: function (d) {
+                    return d.status == 1 ? '<span class="green">启用</span>' : '<span class="yellow">禁用</span>';
+                }},
+                { width: 180, title: '操作', align: 'center', templet: function (d) {
+                    var edit = '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit">编辑</a>';
+                    var disable = '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="disable">禁用</a>';
+                    var enable = '<a class="layui-btn layui-btn-xs" lay-event="open">启用</a>';
+                    var del = '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>';
+                    var toggle = d.status == 1 ? disable : enable;
+                    return '<div class="layui-btn-group">' + edit + toggle + del + '</div>';
+                }}
+            ]]
+        });
+
+        table.on('tool(table_currency)', function (obj) {
+            if (obj.event === 'edit') {
+                editCurrency(obj.data);
+            }
+            if (obj.event === 'disable') {
+                layer.confirm('确定要禁用该币种吗?', { icon: 3 }, function (index) {
+                    tool.post('/home/currency/set', { id: obj.data.id, status: 0 }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) layui.pageTable.reload();
+                    });
+                    layer.close(index);
+                });
+            }
+            if (obj.event === 'open') {
+                layer.confirm('确定要启用该币种吗?', { icon: 3 }, function (index) {
+                    tool.post('/home/currency/set', { id: obj.data.id, status: 1 }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) layui.pageTable.reload();
+                    });
+                    layer.close(index);
+                });
+            }
+            if (obj.event === 'del') {
+                layer.confirm('确定要删除该币种吗?', { icon: 3 }, function (index) {
+                    tool.delete('/home/currency/del', { id: obj.data.id }, function (e) {
+                        layer.msg(e.msg);
+                        if (e.code == 0) layui.pageTable.reload();
+                    });
+                    layer.close(index);
+                });
+            }
+        });
+
+        $('body').on('click', '.add-new', function () {
+            editCurrency();
+        });
+
+        $('#btnSearch').on('click', function () {
+            layui.pageTable.reload({ where: { keywords: $('#keywords').val() } });
+        });
+
+        function editCurrency(row) {
+            row = row || {};
+            var isEdit = row.id > 0;
+            var id     = row.id || 0;
+            var code   = row.code || '';
+            var title  = row.title || '';
+            var sort   = row.sort || 0;
+            layer.open({
+                type: 1,
+                title: isEdit ? '编辑币种' : '添加币种',
+                area: ['360px', 'auto'],
+                content: '<div style="padding:20px">'
+                    + '<div class="layui-form-item"><label>币种代码(如 USD)</label>'
+                    + '<input id="currency_code" class="layui-input" value="' + code + '" placeholder="ISO 4217 代码"></div>'
+                    + '<div class="layui-form-item" style="margin-top:10px"><label>币种名称</label>'
+                    + '<input id="currency_title" class="layui-input" value="' + title + '" placeholder="如 美元"></div>'
+                    + '<div class="layui-form-item" style="margin-top:10px"><label>排序</label>'
+                    + '<input id="currency_sort" class="layui-input" type="number" value="' + sort + '"></div>'
+                    + '</div>',
+                btn: ['确定', '取消'],
+                yes: function (index) {
+                    var cval = $.trim($('#currency_code').val());
+                    var tval = $.trim($('#currency_title').val());
+                    var sval = $('#currency_sort').val();
+                    if (!cval) { layer.msg('请填写币种代码'); return; }
+                    if (!tval) { layer.msg('请填写币种名称'); return; }
+                    tool.post('/home/currency/add',
+                        { id: id, code: cval, title: tval, sort: sval },
+                        function (e) {
+                            layer.msg(e.msg);
+                            if (e.code == 0) { layer.close(index); layui.pageTable.reload(); }
+                        }
+                    );
+                }
+            });
+        }
+    }
+</script>
+{/block}

+ 1 - 1
app/user/controller/User.php

@@ -236,7 +236,7 @@ class User extends BaseController
         $Avatar = new MDAvatars($Char, 256, 1);
         $avatar_name = '/avatars/avatar_256_' . set_salt(10) . time() . '.png';
         $path = get_config('filesystem.disks.public.url') . $avatar_name;
-        $res = $Avatar->Save('.' . $path, 256);
+        $res = $Avatar->Save(public_path() . ltrim($path, '/'), 256);
         $Avatar->Free();
         return $path;
     }

+ 2 - 2
database/migrations/20260525061909_seed_price_adjust_data.php

@@ -40,7 +40,7 @@ class SeedPriceAdjustData extends Migrator
              `form`, `add_url`, `view_url`, `form_id`, `is_list`, `status`,
              `template_id`, `create_time`, `update_time`)
             VALUES
-            ('调价申请', 'price_adjust', {$flowModuleId}, 'price_adjust', 'icon-tiaojia',
+            ('调价申请', 'price_adjust', {$flowModuleId}, 'price_adjust', 'icon-zhangbu',
              '', 50, 1, 1, 0, 1, 0,
              1, '/finance/priceadjust/add', '/finance/priceadjust/view', 0, 1, 1,
              0, {$now}, 0)");
@@ -81,7 +81,7 @@ class SeedPriceAdjustData extends Migrator
         $this->execute("INSERT INTO `oa_admin_rule`
             (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
             VALUES
-            ({$financePid}, 'finance/priceadjust/datalist', '调价申请', '调价申请', 'finance', 'icon-tiaojia', 1, 50, 1, {$now})");
+            ({$financePid}, 'finance/priceadjust/datalist', '调价申请', '调价申请', 'finance', 'icon-zhangbu', 1, 50, 1, {$now})");
 
         $ruleId = $db->lastInsertId();
 

+ 2 - 2
database/migrations/20260525070002_seed_credit_writeoff_data.php

@@ -40,7 +40,7 @@ class SeedCreditWriteoffData extends Migrator
              `form`, `add_url`, `view_url`, `form_id`, `is_list`, `status`,
              `template_id`, `create_time`, `update_time`)
             VALUES
-            ('授信&核销申请', 'credit_writeoff', {$flowModuleId}, 'credit_writeoff', 'icon-shoukuanzuofei',
+            ('授信&核销申请', 'credit_writeoff', {$flowModuleId}, 'credit_writeoff', 'icon-a-baoxiao2',
              '', 51, 1, 1, 0, 1, 0,
              1, '/finance/creditwriteoff/add', '/finance/creditwriteoff/view', 0, 1, 1,
              0, {$now}, 0)");
@@ -81,7 +81,7 @@ class SeedCreditWriteoffData extends Migrator
         $this->execute("INSERT INTO `oa_admin_rule`
             (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
             VALUES
-            ({$financePid}, 'finance/creditwriteoff/datalist', '授信&核销申请', '授信&核销申请', 'finance', 'icon-shoukuanzuofei', 1, 51, 1, {$now})");
+            ({$financePid}, 'finance/creditwriteoff/datalist', '授信&核销申请', '授信&核销申请', 'finance', 'icon-a-baoxiao2', 1, 51, 1, {$now})");
 
         $ruleId = $db->lastInsertId();
 

+ 27 - 0
database/migrations/20260525090001_create_currency.php

@@ -0,0 +1,27 @@
+<?php
+
+use Phinx\Db\Adapter\MysqlAdapter;
+use think\migration\Migrator;
+
+class CreateCurrency extends Migrator
+{
+    public function change(): void {
+        $table = $this->table('currency', [
+            'id'          => 'id',
+            'engine'      => 'InnoDB',
+            'collation'   => 'utf8mb4_general_ci',
+            'comment'     => '币种字典表',
+            'auto_id'     => true,
+        ]);
+
+        $table
+            ->addColumn('code', 'string', ['limit' => 8, 'null' => false, 'default' => '', 'comment' => '币种代码(ISO 4217,业务唯一键)'])
+            ->addColumn('title', 'string', ['limit' => 50, 'null' => false, 'default' => '', 'comment' => '币种名称'])
+            ->addColumn('sort', 'integer', ['null' => false, 'default' => 0, 'comment' => '排序'])
+            ->addColumn('status', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 1, 'comment' => '状态:0禁用,1启用'])
+            ->addColumn('create_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '创建时间'])
+            ->addColumn('delete_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '删除时间'])
+            ->addIndex(['code'], ['name' => 'idx_code'])
+            ->create();
+    }
+}

+ 69 - 0
database/migrations/20260525090002_seed_currency_data.php

@@ -0,0 +1,69 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedCurrencyData extends Migrator
+{
+    public function up(): void
+    {
+        $now = time();
+
+        // [code, title] —— 常用币种在前,按 sort 递增
+        $rows = [
+            ['CNY', '人民币'], ['USD', '美元'], ['EUR', '欧元'], ['GBP', '英镑'], ['JPY', '日元'],
+            ['HKD', '港元'], ['AUD', '澳大利亚元'], ['CAD', '加拿大元'], ['CHF', '瑞士法郎'], ['SGD', '新加坡元'],
+            ['KRW', '韩元'], ['THB', '泰铢'], ['VND', '越南盾'], ['MYR', '马来西亚林吉特'], ['IDR', '印尼盾'],
+            ['PHP', '菲律宾比索'], ['INR', '印度卢比'], ['NZD', '新西兰元'], ['RUB', '俄罗斯卢布'], ['AED', '阿联酋迪拉姆'],
+            ['SAR', '沙特里亚尔'], ['QAR', '卡塔尔里亚尔'], ['MOP', '澳门元'], ['TWD', '新台币'], ['ZAR', '南非兰特'],
+            ['BRL', '巴西雷亚尔'], ['MXN', '墨西哥比索'], ['TRY', '土耳其里拉'], ['PLN', '波兰兹罗提'], ['SEK', '瑞典克朗'],
+            ['NOK', '挪威克朗'], ['DKK', '丹麦克朗'], ['CZK', '捷克克朗'], ['HUF', '匈牙利福林'], ['RON', '罗马尼亚列伊'],
+            ['KZT', '哈萨克斯坦坚戈'], ['UAH', '乌克兰格里夫纳'], ['ILS', '以色列新谢克尔'], ['OMR', '阿曼里亚尔'], ['KWD', '科威特第纳尔'],
+            ['BHD', '巴林第纳尔'], ['JOD', '约旦第纳尔'], ['LBP', '黎巴嫩镑'], ['EGP', '埃及镑'], ['IRR', '伊朗里亚尔'],
+            ['YER', '也门里亚尔'], ['IQD', '伊拉克第纳尔'], ['SYP', '叙利亚镑'], ['AFN', '阿富汗尼'], ['PKR', '巴基斯坦卢比'],
+            ['LKR', '斯里兰卡卢比'], ['BDT', '孟加拉塔卡'], ['KHR', '柬埔寨瑞尔'], ['LAK', '老挝基普'], ['MMK', '缅甸元'],
+            ['MNT', '蒙古图格里克'], ['KPW', '朝鲜圆'], ['GEL', '格鲁吉亚拉里'], ['AZN', '阿塞拜疆马纳特'], ['AMD', '亚美尼亚德拉姆'],
+            ['TJS', '塔吉克斯坦索莫尼'], ['TMT', '土库曼斯坦马纳特'], ['UZS', '乌兹别克斯坦苏姆'], ['KGS', '吉尔吉斯斯坦索姆'], ['ARS', '阿根廷比索'],
+            ['CLP', '智利比索'], ['COP', '哥伦比亚比索'], ['PEN', '秘鲁索尔'], ['UYU', '乌拉圭比索'], ['PYG', '巴拉圭瓜拉尼'],
+            ['BOB', '玻利维亚诺'], ['CRC', '哥斯达黎加科朗'], ['DOP', '多米尼加比索'], ['NIO', '尼加拉瓜科多巴'], ['PAB', '巴拿马巴波亚'],
+            ['SVC', '萨尔瓦多科朗'], ['BZD', '伯利兹元'], ['GTQ', '危地马拉格查尔'], ['HNL', '洪都拉斯伦皮拉'], ['BSD', '巴哈马元'],
+            ['BBD', '巴巴多斯元'], ['BMD', '百慕大元'], ['KYD', '开曼元'], ['TTD', '特立尼达多巴哥元'], ['SRD', '苏里南元'],
+            ['GYD', '圭亚那元'], ['VES', '委内瑞拉玻利瓦尔'], ['DZD', '阿尔及利亚第纳尔'], ['MAD', '摩洛哥迪拉姆'], ['TND', '突尼斯第纳尔'],
+            ['LYD', '利比亚第纳尔'], ['SDG', '苏丹镑'], ['ETB', '埃塞俄比亚比尔'], ['ERN', '厄立特里亚纳克法'], ['KES', '肯尼亚先令'],
+            ['TZS', '坦桑尼亚先令'], ['UGX', '乌干达先令'], ['SOS', '索马里先令'], ['BIF', '布隆迪法郎'], ['RWF', '卢旺达法郎'],
+            ['CDF', '刚果法郎'], ['GNF', '几内亚法郎'], ['SLL', '塞拉利昂利昂'], ['GMD', '冈比亚达拉西'], ['NGN', '尼日利亚奈拉'],
+            ['MRU', '毛里塔尼亚乌吉亚'], ['MUR', '毛里求斯卢比'], ['MVR', '马尔代夫拉菲亚'], ['BWP', '博茨瓦纳普拉'], ['LSL', '莱索托洛蒂'],
+            ['NAD', '纳米比亚元'], ['MWK', '马拉维克瓦查'], ['MGA', '马达加斯加阿里亚里'], ['AOA', '安哥拉宽扎'], ['MZN', '莫桑比克梅蒂卡尔'],
+            ['ZMK', '赞比亚克瓦查'], ['ZWL', '津巴布韦元'], ['XAF', '中非法郎'], ['XOF', '西非法郎'], ['KMF', '科摩罗法郎'],
+            ['CVE', '佛得角埃斯库多'], ['GHS', '加纳塞地'], ['ALL', '阿尔巴尼亚列克'], ['BGN', '保加利亚列弗'], ['BAM', '波黑可兑换马克'],
+            ['HRK', '克罗地亚库纳'], ['MKD', '北马其顿代纳尔'], ['RSD', '塞尔维亚第纳尔'], ['ISK', '冰岛克朗'], ['BYN', '白俄罗斯卢布'],
+            ['MDL', '摩尔多瓦列伊'], ['GGP', '根西岛镑'], ['GIP', '直布罗陀镑'], ['IMP', '马恩岛镑'], ['JEP', '泽西岛镑'],
+            ['FKP', '福克兰群岛镑'], ['FJD', '斐济元'], ['PGK', '巴布亚新几内亚基那'], ['SBD', '所罗门群岛元'], ['TOP', '汤加潘加'],
+            ['TVD', '图瓦卢元'], ['VUV', '瓦努阿图瓦图'], ['WST', '萨摩亚塔拉'], ['XCD', '东加勒比元'], ['XPF', '太平洋法郎'],
+            ['ANG', '荷属安的列斯盾'], ['AWG', '阿鲁巴弗罗林'], ['SCR', '塞舌尔卢比'], ['SHP', '圣赫勒拿镑'], ['STN', '圣多美多布拉'],
+            ['XDR', '特别提款权'], ['XAU', '金盎司'], ['XAG', '银盎司'], ['XPT', '铂金盎司'], ['XPD', '钯金盎司'],
+            ['CUC', '古巴可兑换比索'], ['CUP', '古巴比索'], ['CHE', '欧元瑞士法郎'], ['CHW', '瑞士法郎欧元'], ['CLF', '智利比索基金'],
+            ['COU', '哥伦比亚币'], ['CSD', '旧塞尔维亚第纳尔'], ['ECS', '厄瓜多尔苏克雷'], ['GNS', '几内亚西里'], ['LTL', '立陶宛立特'],
+            ['LVL', '拉脱维亚拉特'], ['MXV', '墨西哥投资比索'], ['ROL', '旧罗马尼亚列伊'], ['SIT', '斯洛文尼亚托拉尔'], ['TMM', '旧土库曼马纳特'],
+            ['UYP', '乌拉圭比索指数'], ['VEB', '旧委内瑞拉博利瓦'], ['XSU', '苏克雷货币单位'], ['ZWN', '旧津巴布韦元'], ['JMD', '牙买加元'],
+            ['BTN', '不丹努尔特鲁姆'], ['SZL', '斯威士兰里兰吉尼'], ['NPR', '尼泊尔卢比'], ['LRD', '利比里亚元'], ['BND', '文莱元'],
+        ];
+
+        $values = [];
+        $sort   = 1;
+        foreach ($rows as $row) {
+            $code  = $this->getAdapter()->getConnection()->quote($row[0]);
+            $title = $this->getAdapter()->getConnection()->quote($row[1]);
+            $values[] = "({$code}, {$title}, {$sort}, 1, {$now}, 0)";
+            $sort++;
+        }
+
+        $sql = "INSERT INTO `oa_currency` (`code`, `title`, `sort`, `status`, `create_time`, `delete_time`) VALUES "
+            . implode(',', $values);
+        $this->execute($sql);
+    }
+
+    public function down(): void
+    {
+        $this->execute("DELETE FROM `oa_currency`");
+    }
+}

+ 87 - 0
database/migrations/20260525090003_seed_currency_rule.php

@@ -0,0 +1,87 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedCurrencyRule extends Migrator
+{
+    public function up(): void
+    {
+        $now = time();
+        $db  = $this->getAdapter()->getConnection();
+
+        // 动态查找「公共模块」(基础数据下,module=home)节点 ID
+        $commonMenu = $db->query("SELECT id FROM `oa_admin_rule` WHERE title='公共模块' AND module='home' LIMIT 1")
+                         ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($commonMenu)) {
+            throw new \RuntimeException('找不到公共模块菜单节点,请确认基础数据已安装');
+        }
+        $commonPid = $commonMenu[0]['id'];
+
+        // 动态查找超级权限角色 ID
+        $superGroup = $db->query("SELECT id FROM `oa_admin_group` WHERE title='超级权限角色' LIMIT 1")
+                         ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($superGroup)) {
+            throw new \RuntimeException('找不到超级权限角色,请确认基础数据已安装');
+        }
+        $superGroupId = $superGroup[0]['id'];
+
+        // 菜单节点
+        $this->execute("INSERT INTO `oa_admin_rule`
+            (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
+            VALUES
+            ({$commonPid}, 'home/currency/datalist', '币种管理', '币种管理', 'home', '', 1, 80, 1, {$now})");
+
+        $ruleId = $db->lastInsertId();
+
+        // 子权限节点
+        $this->execute("INSERT INTO `oa_admin_rule`
+            (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
+            VALUES
+            ({$ruleId}, 'home/currency/datalist', '列表',      '币种管理-列表',     'home', '', 2, 1, 1, {$now}),
+            ({$ruleId}, 'home/currency/add',      '添加/编辑', '币种管理-添加编辑', 'home', '', 2, 2, 1, {$now}),
+            ({$ruleId}, 'home/currency/del',      '删除',      '币种管理-删除',     'home', '', 2, 3, 1, {$now}),
+            ({$ruleId}, 'home/currency/set',      '设置',      '币种管理-设置',     'home', '', 2, 4, 1, {$now})");
+
+        // 授权给超级权限角色
+        $this->execute("UPDATE `oa_admin_group`
+            SET `rules` = CONCAT(`rules`, ',', (
+                SELECT GROUP_CONCAT(t.id) FROM (
+                    SELECT id FROM `oa_admin_rule` WHERE id = {$ruleId} OR pid = {$ruleId}
+                ) t
+            ))
+            WHERE `id` = {$superGroupId}");
+    }
+
+    public function down(): void
+    {
+        $db = $this->getAdapter()->getConnection();
+
+        $ruleIds = $db->query("SELECT id FROM `oa_admin_rule` WHERE src LIKE 'home/currency/%'")
+                      ->fetchAll(\PDO::FETCH_COLUMN);
+        if (!empty($ruleIds)) {
+            $this->removeRulesFromGroups($ruleIds);
+            $in = implode(',', array_map('intval', $ruleIds));
+            $this->execute("DELETE FROM `oa_admin_rule` WHERE id IN ({$in})");
+        }
+    }
+
+    /**
+     * 从所有角色的 rules 字段中剔除指定权限节点 ID
+     *
+     * @param array<int, int|string> $ruleIds
+     */
+    protected function removeRulesFromGroups(array $ruleIds): void
+    {
+        $db      = $this->getAdapter()->getConnection();
+        $ruleIds = array_map('strval', $ruleIds);
+        $groups  = $db->query("SELECT id, rules FROM `oa_admin_group`")->fetchAll(\PDO::FETCH_ASSOC);
+        foreach ($groups as $group) {
+            $rules = array_filter(explode(',', (string) $group['rules']), static fn($v) => $v !== '');
+            $kept  = array_values(array_diff($rules, $ruleIds));
+            if (count($kept) !== count($rules)) {
+                $newRules = implode(',', $kept);
+                $this->execute("UPDATE `oa_admin_group` SET `rules` = '{$newRules}' WHERE `id` = {$group['id']}");
+            }
+        }
+    }
+}

+ 44 - 0
database/migrations/20260525090004_create_vcc_payment.php

@@ -0,0 +1,44 @@
+<?php
+
+use Phinx\Db\Adapter\MysqlAdapter;
+use think\migration\Migrator;
+
+class CreateVccPayment extends Migrator
+{
+    public function change(): void {
+        $table = $this->table('vcc_payment', [
+            'id'          => 'id',
+            'engine'      => 'InnoDB',
+            'collation'   => 'utf8mb4_general_ci',
+            'comment'     => 'VCC返点/提成付款申请表',
+            'auto_id'     => true,
+        ]);
+
+        $table
+            // 业务字段
+            ->addColumn('payment_reason', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '付款事由'])
+            ->addColumn('amount', 'decimal', ['precision' => 14, 'scale' => 2, 'null' => false, 'default' => 0, 'comment' => '金额'])
+            ->addColumn('currency', 'string', ['limit' => 8, 'null' => false, 'default' => '', 'comment' => '币种代码(来自币种管理)'])
+            ->addColumn('payment_method', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '付款方式:1现金,2银行卡,3支票,4电汇,5汇票,6贷记,7其他'])
+            ->addColumn('payment_date', 'string', ['limit' => 20, 'null' => false, 'default' => '', 'comment' => '付款日期'])
+            ->addColumn('bank_account', 'string', ['limit' => 255, 'null' => false, 'default' => '', 'comment' => '银行账户'])
+            ->addColumn('remark', 'text', ['null' => true, 'comment' => '备注'])
+            ->addColumn('file_ids', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '附件id,id,id'])
+            // 审批必填字段
+            ->addColumn('check_status', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '审核状态:0待审核,1审核中,2通过,3不通过,4撤销'])
+            ->addColumn('check_flow_id', 'integer', ['null' => false, 'default' => 0, 'comment' => '审核流程id'])
+            ->addColumn('check_step_sort', 'integer', ['null' => false, 'default' => 0, 'comment' => '当前审批步骤'])
+            ->addColumn('check_uids', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '当前审批人ID'])
+            ->addColumn('check_last_uid', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '上一审批人'])
+            ->addColumn('check_history_uids', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '历史审批人ID'])
+            ->addColumn('check_copy_uids', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '抄送人ID'])
+            ->addColumn('check_time', 'biginteger', ['null' => false, 'default' => 0, 'signed' => false, 'comment' => '审核通过时间'])
+            // 用户/部门/时间戳
+            ->addColumn('admin_id', 'integer', ['null' => false, 'default' => 0, 'comment' => '创建人ID'])
+            ->addColumn('did', 'integer', ['null' => false, 'default' => 0, 'comment' => '创建人部门ID'])
+            ->addColumn('create_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '创建时间'])
+            ->addColumn('update_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '更新时间'])
+            ->addColumn('delete_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '删除时间'])
+            ->create();
+    }
+}

+ 151 - 0
database/migrations/20260525090005_seed_vcc_payment_data.php

@@ -0,0 +1,151 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedVccPaymentData extends Migrator
+{
+    public function up(): void
+    {
+        $now = time();
+        $db  = $this->getAdapter()->getConnection();
+
+        // 动态查找财务管理菜单节点 ID
+        $financeMenu = $db->query("SELECT id FROM `oa_admin_rule` WHERE title='财务管理' AND module='finance' AND pid=0 LIMIT 1")
+                          ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($financeMenu)) {
+            throw new \RuntimeException('找不到财务管理菜单节点,请确认基础数据已安装');
+        }
+        $financePid = $financeMenu[0]['id'];
+
+        // 动态查找审批流程财务模块 ID(oa_flow_module.title='财务')
+        $flowModule = $db->query("SELECT id FROM `oa_flow_module` WHERE title='财务' LIMIT 1")
+                         ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($flowModule)) {
+            throw new \RuntimeException('找不到审批流程财务模块,请确认基础数据已安装');
+        }
+        $flowModuleId = $flowModule[0]['id'];
+
+        // 动态查找超级权限角色 ID
+        $superGroup = $db->query("SELECT id FROM `oa_admin_group` WHERE title='超级权限角色' LIMIT 1")
+                         ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($superGroup)) {
+            throw new \RuntimeException('找不到超级权限角色,请确认基础数据已安装');
+        }
+        $superGroupId = $superGroup[0]['id'];
+
+        // 1. 注册审批类型
+        $this->execute("INSERT INTO `oa_flow_cate`
+            (`title`, `name`, `module_id`, `check_table`, `icon`,
+             `department_ids`, `sort`, `is_copy`, `is_file`, `is_export`, `is_back`, `is_reversed`,
+             `form`, `add_url`, `view_url`, `form_id`, `is_list`, `status`,
+             `template_id`, `create_time`, `update_time`)
+            VALUES
+            ('VCC返点/提成付款申请', 'vcc_payment', {$flowModuleId}, 'vcc_payment', 'icon-fukuanshenqing',
+             '', 52, 1, 1, 0, 1, 0,
+             1, '/finance/vccpayment/add', '/finance/vccpayment/view', 0, 1, 1,
+             0, {$now}, 0)");
+
+        $cateId = $db->lastInsertId();
+
+        // 2. 创建默认审批流程(自由审批流)
+        $this->execute("INSERT INTO `oa_flow`
+            (`title`, `cate_id`, `check_type`, `department_ids`, `copy_uids`, `flow_list`,
+             `status`, `remark`, `admin_id`, `create_time`)
+            VALUES
+            ('VCC返点/提成付款申请审批', {$cateId}, 1, '', '', '', 1, '', 1, {$now})");
+
+        // 3. 注册消息模板
+        $this->execute("INSERT INTO `oa_template`
+            (`title`, `name`, `types`, `check_types`, `remark`, `msg_link`,
+             `msg_title_0`, `msg_content_0`,
+             `msg_title_1`, `msg_content_1`,
+             `msg_title_2`, `msg_content_2`,
+             `msg_title_3`, `msg_content_3`,
+             `email_link`, `status`, `admin_id`, `create_time`, `update_time`)
+            VALUES
+            ('VCC返点/提成付款申请审批', 'vcc_payment', 2, 0, '', '/finance/vccpayment/view/id/{action_id}',
+             '{from_user}提交了一个『VCC返点/提成付款申请』,请及时审批',
+             '您有一个新的『VCC返点/提成付款申请』需要处理。',
+             '您提交的『VCC返点/提成付款申请』已被审批通过。',
+             '您在{create_time}提交的『VCC返点/提成付款申请』已于{date}被审批通过。',
+             '您提交的『VCC返点/提成付款申请』已被驳回拒绝。',
+             '您在{create_time}提交的『VCC返点/提成付款申请』已于{date}被驳回拒绝。',
+             '{from_user}提交的『VCC返点/提成付款申请』已被审批通过并抄送给你',
+             '{from_user}在{create_time}提交的『VCC返点/提成付款申请』已被审批通过并抄送给你,请及时查看详情。',
+             '', 1, 1, {$now}, 0)");
+
+        $tplId = $db->lastInsertId();
+        $this->execute("UPDATE `oa_flow_cate` SET `template_id` = {$tplId} WHERE `id` = {$cateId}");
+
+        // 4. 菜单 + 权限节点
+        $this->execute("INSERT INTO `oa_admin_rule`
+            (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
+            VALUES
+            ({$financePid}, 'finance/vccpayment/datalist', 'VCC返点/提成付款申请', 'VCC返点/提成付款申请', 'finance', 'icon-fukuanshenqing', 1, 52, 1, {$now})");
+
+        $ruleId = $db->lastInsertId();
+
+        $this->execute("INSERT INTO `oa_admin_rule`
+            (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
+            VALUES
+            ({$ruleId}, 'finance/vccpayment/datalist', '列表',      'VCC付款申请-列表',     'finance', '', 2, 1, 1, {$now}),
+            ({$ruleId}, 'finance/vccpayment/add',      '添加/编辑', 'VCC付款申请-添加编辑', 'finance', '', 2, 2, 1, {$now}),
+            ({$ruleId}, 'finance/vccpayment/view',     '详情',      'VCC付款申请-详情',     'finance', '', 2, 3, 1, {$now}),
+            ({$ruleId}, 'finance/vccpayment/del',      '删除',      'VCC付款申请-删除',     'finance', '', 2, 4, 1, {$now})");
+
+        // 5. 授权给超级权限角色
+        $this->execute("UPDATE `oa_admin_group`
+            SET `rules` = CONCAT(`rules`, ',', (
+                SELECT GROUP_CONCAT(t.id) FROM (
+                    SELECT id FROM `oa_admin_rule` WHERE id = {$ruleId} OR pid = {$ruleId}
+                ) t
+            ))
+            WHERE `id` = {$superGroupId}");
+    }
+
+    public function down(): void
+    {
+        $db = $this->getAdapter()->getConnection();
+
+        // 1. 回收权限节点:先从超级权限角色 rules 中剔除,再删除节点
+        $ruleIds = $db->query("SELECT id FROM `oa_admin_rule` WHERE src LIKE 'finance/vccpayment/%'")
+                      ->fetchAll(\PDO::FETCH_COLUMN);
+        if (!empty($ruleIds)) {
+            $this->removeRulesFromGroups($ruleIds);
+            $in = implode(',', array_map('intval', $ruleIds));
+            $this->execute("DELETE FROM `oa_admin_rule` WHERE id IN ({$in})");
+        }
+
+        // 2. 删除审批流程(依赖审批类型 cate_id,需先删)
+        $cateIds = $db->query("SELECT id FROM `oa_flow_cate` WHERE name='vcc_payment'")
+                      ->fetchAll(\PDO::FETCH_COLUMN);
+        if (!empty($cateIds)) {
+            $in = implode(',', array_map('intval', $cateIds));
+            $this->execute("DELETE FROM `oa_flow` WHERE `cate_id` IN ({$in})");
+        }
+
+        // 3. 删除消息模板与审批类型
+        $this->execute("DELETE FROM `oa_template` WHERE `name` = 'vcc_payment'");
+        $this->execute("DELETE FROM `oa_flow_cate` WHERE `name` = 'vcc_payment'");
+    }
+
+    /**
+     * 从所有角色的 rules 字段中剔除指定权限节点 ID
+     *
+     * @param array<int, int|string> $ruleIds
+     */
+    protected function removeRulesFromGroups(array $ruleIds): void
+    {
+        $db      = $this->getAdapter()->getConnection();
+        $ruleIds = array_map('strval', $ruleIds);
+        $groups  = $db->query("SELECT id, rules FROM `oa_admin_group`")->fetchAll(\PDO::FETCH_ASSOC);
+        foreach ($groups as $group) {
+            $rules = array_filter(explode(',', (string) $group['rules']), static fn($v) => $v !== '');
+            $kept  = array_values(array_diff($rules, $ruleIds));
+            if (count($kept) !== count($rules)) {
+                $newRules = implode(',', $kept);
+                $this->execute("UPDATE `oa_admin_group` SET `rules` = '{$newRules}' WHERE `id` = {$group['id']}");
+            }
+        }
+    }
+}

+ 26 - 0
database/migrations/20260525090006_create_vcc_payment_cate.php

@@ -0,0 +1,26 @@
+<?php
+
+use Phinx\Db\Adapter\MysqlAdapter;
+use think\migration\Migrator;
+
+class CreateVccPaymentCate extends Migrator
+{
+    public function change(): void {
+        $table = $this->table('vcc_payment_cate', [
+            'engine'    => 'InnoDB',
+            'collation' => 'utf8mb4_general_ci',
+            'comment'   => 'VCC返点/提成付款申请类型字典表',
+        ]);
+
+        $table
+            ->addColumn('type', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '类型:1付款方式'])
+            ->addColumn('title', 'string', ['limit' => 100, 'null' => false, 'default' => '', 'comment' => '名称'])
+            ->addColumn('value', 'integer', ['null' => false, 'default' => 0, 'comment' => '业务值(同type内唯一,申请单存此值)'])
+            ->addColumn('is_other', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '是否为其他选项:1是(选中后需填备注)'])
+            ->addColumn('sort', 'integer', ['null' => false, 'default' => 0, 'comment' => '排序'])
+            ->addColumn('status', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 1, 'comment' => '状态:0禁用,1启用'])
+            ->addColumn('create_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '创建时间'])
+            ->addColumn('delete_time', 'biginteger', ['null' => false, 'default' => 0, 'comment' => '删除时间'])
+            ->create();
+    }
+}

+ 37 - 0
database/migrations/20260525090007_seed_vcc_payment_cate_data.php

@@ -0,0 +1,37 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedVccPaymentCateData extends Migrator
+{
+    public function up(): void
+    {
+        $now = time();
+
+        // type=1 付款方式  [type, title, value, is_other, sort]
+        $rows = [
+            [1, '现金',   1, 0, 1],
+            [1, '银行卡', 2, 0, 2],
+            [1, '支票',   3, 0, 3],
+            [1, '电汇',   4, 0, 4],
+            [1, '汇票',   5, 0, 5],
+            [1, '贷记',   6, 0, 6],
+            [1, '其他',   7, 0, 7],
+        ];
+
+        $values = [];
+        foreach ($rows as $row) {
+            $title    = $this->getAdapter()->getConnection()->quote($row[1]);
+            $values[] = "({$row[0]}, {$title}, {$row[2]}, {$row[3]}, {$row[4]}, 1, {$now}, 0)";
+        }
+
+        $sql = "INSERT INTO `oa_vcc_payment_cate` (`type`, `title`, `value`, `is_other`, `sort`, `status`, `create_time`, `delete_time`) VALUES "
+            . implode(',', $values);
+        $this->execute($sql);
+    }
+
+    public function down(): void
+    {
+        $this->execute("DELETE FROM `oa_vcc_payment_cate`");
+    }
+}

+ 87 - 0
database/migrations/20260525090008_seed_vcc_payment_cate_rule.php

@@ -0,0 +1,87 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedVccPaymentCateRule extends Migrator
+{
+    public function up(): void
+    {
+        $now = time();
+        $db  = $this->getAdapter()->getConnection();
+
+        // 动态查找「财务模块」(基础数据-财务管理)节点 ID
+        $financeMenu = $db->query("SELECT id FROM `oa_admin_rule` WHERE title='财务模块' AND module='finance' LIMIT 1")
+                          ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($financeMenu)) {
+            throw new \RuntimeException('找不到财务模块菜单节点,请确认基础数据已安装');
+        }
+        $financePid = $financeMenu[0]['id'];
+
+        // 动态查找超级权限角色 ID
+        $superGroup = $db->query("SELECT id FROM `oa_admin_group` WHERE title='超级权限角色' LIMIT 1")
+                         ->fetchAll(\PDO::FETCH_ASSOC);
+        if (empty($superGroup)) {
+            throw new \RuntimeException('找不到超级权限角色,请确认基础数据已安装');
+        }
+        $superGroupId = $superGroup[0]['id'];
+
+        // 菜单节点
+        $this->execute("INSERT INTO `oa_admin_rule`
+            (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
+            VALUES
+            ({$financePid}, 'finance/vccpaymentcate/datalist', 'VCC付款类型', 'VCC付款类型管理', 'finance', '', 1, 52, 1, {$now})");
+
+        $ruleId = $db->lastInsertId();
+
+        // 子权限节点
+        $this->execute("INSERT INTO `oa_admin_rule`
+            (`pid`, `src`, `title`, `name`, `module`, `icon`, `menu`, `sort`, `status`, `create_time`)
+            VALUES
+            ({$ruleId}, 'finance/vccpaymentcate/datalist', '列表',      'VCC付款类型-列表',     'finance', '', 2, 1, 1, {$now}),
+            ({$ruleId}, 'finance/vccpaymentcate/add',      '添加/编辑', 'VCC付款类型-添加编辑', 'finance', '', 2, 2, 1, {$now}),
+            ({$ruleId}, 'finance/vccpaymentcate/del',      '删除',      'VCC付款类型-删除',     'finance', '', 2, 3, 1, {$now}),
+            ({$ruleId}, 'finance/vccpaymentcate/set',      '设置',      'VCC付款类型-设置',     'finance', '', 2, 4, 1, {$now})");
+
+        // 授权给超级权限角色
+        $this->execute("UPDATE `oa_admin_group`
+            SET `rules` = CONCAT(`rules`, ',', (
+                SELECT GROUP_CONCAT(t.id) FROM (
+                    SELECT id FROM `oa_admin_rule` WHERE id = {$ruleId} OR pid = {$ruleId}
+                ) t
+            ))
+            WHERE `id` = {$superGroupId}");
+    }
+
+    public function down(): void
+    {
+        $db = $this->getAdapter()->getConnection();
+
+        $ruleIds = $db->query("SELECT id FROM `oa_admin_rule` WHERE src LIKE 'finance/vccpaymentcate/%'")
+                      ->fetchAll(\PDO::FETCH_COLUMN);
+        if (!empty($ruleIds)) {
+            $this->removeRulesFromGroups($ruleIds);
+            $in = implode(',', array_map('intval', $ruleIds));
+            $this->execute("DELETE FROM `oa_admin_rule` WHERE id IN ({$in})");
+        }
+    }
+
+    /**
+     * 从所有角色的 rules 字段中剔除指定权限节点 ID
+     *
+     * @param array<int, int|string> $ruleIds
+     */
+    protected function removeRulesFromGroups(array $ruleIds): void
+    {
+        $db      = $this->getAdapter()->getConnection();
+        $ruleIds = array_map('strval', $ruleIds);
+        $groups  = $db->query("SELECT id, rules FROM `oa_admin_group`")->fetchAll(\PDO::FETCH_ASSOC);
+        foreach ($groups as $group) {
+            $rules = array_filter(explode(',', (string) $group['rules']), static fn($v) => $v !== '');
+            $kept  = array_values(array_diff($rules, $ruleIds));
+            if (count($kept) !== count($rules)) {
+                $newRules = implode(',', $kept);
+                $this->execute("UPDATE `oa_admin_group` SET `rules` = '{$newRules}' WHERE `id` = {$group['id']}");
+            }
+        }
+    }
+}

+ 3 - 3
extend/avatars/MDAvatars.php

@@ -34,10 +34,10 @@ class MDAvatars
         $this->AvatarSize      = $AvatarSize;
         $this->Shape           = $Shape;
         $this->Padding         = 30 * ($this->AvatarSize / 256);
-	    $this->LetterFont      = './static/font/MiSans-Regular.ttf';
-        $this->AsianFont       = './static/font/MiSans-Regular.ttf';
+	    $this->LetterFont      = public_path() . 'static/font/MiSans-Regular.ttf';
+        $this->AsianFont       = public_path() . 'static/font/MiSans-Regular.ttf';
         $this->EnableAsianChar = is_file($this->AsianFont);
-        $path='./storage/avatars/';
+        $path=public_path('storage/avatars');
         if(!is_dir($path)){
             mkdir($path, 0755, true);
         }