Преглед на файлове

✨ feat(finance): 新增价格调整及分类管理功能

价格调整模块:
- 新增 PriceAdjust 控制器/模型/验证器,支持调价单的增查与列表
- 新增 add / datalist / view 三个视图页面
- 新增建表迁移 create_price_adjust 及数据填充迁移 seed_price_adjust_data

价格调整分类模块:
- 新增 PriceAdjustCate 控制器/模型及列表视图
- 新增建表迁移 create_price_adjust_cate
- 新增分类数据与分类规则填充迁移 seed_price_adjust_cate_data / seed_price_adjust_cate_rule

基础设施:
- config/database.php 连接配置改为从 .env [DATABASE] 段读取,移除硬编码凭据
- composer 引入 topthink/think-migration ^3.1 以支持数据库迁移
- .env.development 增加 [DATABASE] 段示例
lxz преди 3 седмици
родител
ревизия
0c33c4502b

+ 18 - 0
.env.development

@@ -0,0 +1,18 @@
+[DATABASE]
+TYPE=mysql
+HOSTNAME=
+DATABASE=
+USERNAME=
+PASSWORD=
+HOSTPORT=
+PREFIX=
+CHARSET=
+DEBUG=
+
+[CACHE]
+DRIVER=redis
+HOST=
+PORT=
+PASSWORD=
+SELECT=
+PREFIX=

+ 1 - 1
app/finance/common.php

@@ -76,4 +76,4 @@ function isAuthLoan($uid)
 	$map[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',conf_5)")];
     $count = Db::name('DataAuth')->where($map)->count();
     return $count;
-}
+}

+ 140 - 0
app/finance/controller/PriceAdjust.php

@@ -0,0 +1,140 @@
+<?php
+declare(strict_types=1);
+
+namespace app\finance\controller;
+
+use app\base\BaseController;
+use app\finance\model\PriceAdjust as PriceAdjustModel;
+use app\finance\model\PriceAdjustCate;
+use app\finance\validate\PriceAdjustValidate;
+use think\exception\ValidateException;
+use think\facade\Db;
+use think\facade\View;
+
+class PriceAdjust extends BaseController
+{
+    protected $model;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->model = new PriceAdjustModel();
+    }
+
+    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[] = ['customer_id', '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(PriceAdjustValidate::class)->scene('edit')->check($param);
+                } catch (ValidateException $e) {
+                    return to_assign(1, $e->getError());
+                }
+                $this->model->edit($param);
+            } else {
+                try {
+                    validate(PriceAdjustValidate::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,
+                'customer_id'         => '',
+                'history_fee_deduct'  => 1,
+                'trade_scene'         => 0,
+                'trade_scene_remark'  => '',
+                'card_bin'            => 0,
+                'card_bin_remark'     => '',
+                'card_type'           => 0,
+                'fee_recharge'        => '',
+                'fee_card_open'       => '',
+                'fee_other'           => '',
+                'monthly_trade_vol'   => '',
+                'estimated_trade_vol' => '',
+                'competitor_info'     => '',
+                'remark'              => '',
+                'file_ids'            => '',
+                'check_status'        => 0,
+                'check_flow_id'       => 0,
+            ];
+            if ($id > 0) {
+                $detail = $this->model->getById($id);
+            }
+            View::assign('detail', $detail);
+            View::assign('trade_scenes', PriceAdjustCate::getByType(1));
+            View::assign('card_bins', PriceAdjustCate::getByType(2));
+            View::assign('card_types', PriceAdjustCate::getByType(3));
+            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/PriceAdjustCate.php

@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+
+namespace app\finance\controller;
+
+use app\base\BaseController;
+use app\finance\model\PriceAdjustCate as PriceAdjustCateModel;
+use think\facade\View;
+
+class PriceAdjustCate extends BaseController
+{
+    protected $model;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->model = new PriceAdjustCateModel();
+    }
+
+    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, '错误的请求');
+    }
+}

+ 89 - 0
app/finance/model/PriceAdjust.php

@@ -0,0 +1,89 @@
+<?php
+namespace app\finance\model;
+
+use think\Model;
+use think\facade\Db;
+
+class PriceAdjust 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 {
+            $list = self::where($where)
+                ->where(function ($query) use ($whereOr) {
+                    if (!empty($whereOr)) {
+                        $query->whereOr($whereOr);
+                    }
+                })
+                ->order($order)
+                ->paginate(['list_rows' => $rows])
+                ->each(function ($item) {
+                    $item->check_status_str = check_status_name($item->check_status);
+                    $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, '调价申请');
+        } 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, '调价申请');
+        } 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['trade_scene_name'] = Db::name('PriceAdjustCate')->where(['type' => 1, 'value' => $info['trade_scene'], 'delete_time' => 0])->value('title') ?? '';
+        $info['card_bin_name']    = Db::name('PriceAdjustCate')->where(['type' => 2, 'value' => $info['card_bin'], 'delete_time' => 0])->value('title') ?? '';
+        $info['card_type_name']   = Db::name('PriceAdjustCate')->where(['type' => 3, 'value' => $info['card_type'], '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/PriceAdjustCate.php

@@ -0,0 +1,71 @@
+<?php
+namespace app\finance\model;
+
+use think\Model;
+
+class PriceAdjustCate 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();
+    }
+}

+ 42 - 0
app/finance/validate/PriceAdjustValidate.php

@@ -0,0 +1,42 @@
+<?php
+namespace app\finance\validate;
+
+use think\Validate;
+
+class PriceAdjustValidate extends Validate
+{
+    protected $rule = [
+        'customer_id'        => 'require|max:100',
+        'history_fee_deduct' => 'require|in:0,1',
+        'trade_scene'        => 'require|in:1,2,3,4,5,6,7,8,9',
+        'trade_scene_remark' => 'requireIf:trade_scene,9',
+        'card_bin'           => 'require|in:1,2,3,4,5',
+        'card_type'          => 'require|in:1,2',
+        'fee_recharge'       => 'require',
+        'fee_card_open'      => 'require',
+        'fee_other'          => 'require',
+        'monthly_trade_vol'  => 'require',
+        'estimated_trade_vol'=> 'require',
+        'competitor_info'    => 'require',
+    ];
+
+    protected $message = [
+        'customer_id.require'         => '客户ID不能为空',
+        'history_fee_deduct.require'  => '请选择历史费用是否需要补扣',
+        'trade_scene.require'         => '请选择客户交易主要场景',
+        'trade_scene_remark.requireIf'=> '选择【其他】场景时,交易场景备注不能为空',
+        'card_bin.require'            => '请选择卡BIN',
+        'card_type.require'           => '请选择卡类型',
+        'fee_recharge.require'        => '调整的费项【充值】不能为空',
+        'fee_card_open.require'       => '调整的费项【开卡】不能为空',
+        'fee_other.require'           => '调整的其他费项不能为空',
+        'monthly_trade_vol.require'   => '客户月均整体交易量不能为空',
+        'estimated_trade_vol.require' => '预估切换交易量不能为空',
+        'competitor_info.require'     => '友商名称和报价不能为空',
+    ];
+
+    protected $scene = [
+        'add'  => ['customer_id', 'history_fee_deduct', 'trade_scene', 'trade_scene_remark', 'card_bin', 'card_type', 'fee_recharge', 'fee_card_open', 'fee_other', 'monthly_trade_vol', 'estimated_trade_vol', 'competitor_info'],
+        'edit' => ['customer_id', 'history_fee_deduct', 'trade_scene', 'trade_scene_remark', 'card_bin', 'card_type', 'fee_recharge', 'fee_card_open', 'fee_other', 'monthly_trade_vol', 'estimated_trade_vol', 'competitor_info'],
+    ];
+}

+ 206 - 0
app/finance/view/priceadjust/add.html

@@ -0,0 +1,206 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<form class="layui-form p-page" lay-filter="form-priceadjust">
+    <h3 class="pb-2">调价申请</h3>
+    <table class="layui-table layui-table-form">
+        <tr>
+            <td class="layui-td-gray-2">客户ID<font>*</font></td>
+            <td colspan="3">
+                <input type="text" name="customer_id" class="layui-input"
+                       value="{$detail.customer_id|default=''}"
+                       lay-verify="required" lay-reqText="请输入客户ID"
+                       placeholder="请输入客户ID">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">历史费用是否需要补扣<font>*</font></td>
+            <td colspan="3">
+                <input type="radio" name="history_fee_deduct" value="1" title="是"
+                    {eq name="$detail.history_fee_deduct" value="1"} checked{/eq}
+                    {empty name="$detail"} checked{/empty}>
+                <input type="radio" name="history_fee_deduct" value="0" title="否"
+                    {eq name="$detail.history_fee_deduct" value="0"} checked{/eq}>
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">客户交易主要场景<font>*</font></td>
+            <td colspan="3">
+                <select name="trade_scene" lay-verify="required" lay-reqText="请选择客户交易主要场景" lay-filter="trade_scene">
+                    <option value="">-- 请选择 --</option>
+                    {volist name="$trade_scenes" id="vo"}
+                    <option value="{$vo.value}" data-is-other="{$vo.is_other}" {eq name="$detail.trade_scene" value="$vo.value"} selected{/eq}>{$vo.title}</option>
+                    {/volist}
+                </select>
+            </td>
+        </tr>
+        <tr id="row_trade_scene_remark" style="display:none;">
+            <td class="layui-td-gray-2">交易场景备注<font>*</font></td>
+            <td colspan="3">
+                <input type="text" name="trade_scene_remark" class="layui-input"
+                       value="{$detail.trade_scene_remark|default=''}"
+                       placeholder="选择【其他】时必填,请说明具体场景">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">卡BIN<font>*</font></td>
+            <td>
+                <select name="card_bin" lay-verify="required" lay-reqText="请选择卡BIN">
+                    <option value="">-- 请选择 --</option>
+                    {volist name="$card_bins" id="vo"}
+                    <option value="{$vo.value}" {eq name="$detail.card_bin" value="$vo.value"} selected{/eq}>{$vo.title}</option>
+                    {/volist}
+                </select>
+            </td>
+            <td class="layui-td-gray">卡BIN备注</td>
+            <td>
+                <input type="text" name="card_bin_remark" class="layui-input"
+                       value="{$detail.card_bin_remark|default=''}"
+                       placeholder="选填">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">卡类型<font>*</font></td>
+            <td colspan="3">
+                <select name="card_type" lay-verify="required" lay-reqText="请选择卡类型">
+                    <option value="">-- 请选择 --</option>
+                    {volist name="$card_types" id="vo"}
+                    <option value="{$vo.value}" {eq name="$detail.card_type" 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="fee_recharge" class="layui-input"
+                       value="{$detail.fee_recharge|default=''}"
+                       lay-verify="required" lay-reqText="请填写调整的费项【充值】"
+                       placeholder="如不变请填写 /">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">调整的费项【开卡】<font>*</font></td>
+            <td colspan="3">
+                <input type="text" name="fee_card_open" class="layui-input"
+                       value="{$detail.fee_card_open|default=''}"
+                       lay-verify="required" lay-reqText="请填写调整的费项【开卡】"
+                       placeholder="如不变请填写 /">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">调整的其他费项<font>*</font></td>
+            <td colspan="3">
+                <input type="text" name="fee_other" class="layui-input"
+                       value="{$detail.fee_other|default=''}"
+                       lay-verify="required" lay-reqText="请填写调整的其他费项"
+                       placeholder="如无其他费项请填写 无">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">客户月均整体交易量<font>*</font></td>
+            <td>
+                <input type="text" name="monthly_trade_vol" class="layui-input"
+                       value="{$detail.monthly_trade_vol|default=''}"
+                       lay-verify="required" lay-reqText="请填写客户月均整体交易量"
+                       placeholder="单位:万美金">
+            </td>
+            <td class="layui-td-gray">预估切换交易量<font>*</font></td>
+            <td>
+                <input type="text" name="estimated_trade_vol" class="layui-input"
+                       value="{$detail.estimated_trade_vol|default=''}"
+                       lay-verify="required" lay-reqText="请填写预估切换交易量"
+                       placeholder="单位:万美金">
+            </td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">目前使用的友商名称和报价<font>*</font></td>
+            <td colspan="3">
+                <textarea name="competitor_info" class="layui-textarea"
+                          lay-verify="required" lay-reqText="请填写友商名称和报价"
+                          placeholder="请填写目前使用的友商名称和报价">{$detail.competitor_info|default=''}</textarea>
+            </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: 'price_adjust', check_btn: 0 });
+        var fileUp = new uploadPlus();
+
+        // 交易场景选择【其他】时显示备注(由 is_other 字段控制)
+        function checkTradeSceneRemark() {
+            var isOther = $('select[name=trade_scene] option:selected').data('is-other');
+            if (isOther == 1) {
+                $('#row_trade_scene_remark').show();
+                $('input[name=trade_scene_remark]').attr('lay-verify', 'required').attr('lay-reqText', '请填写交易场景备注');
+            } else {
+                $('#row_trade_scene_remark').hide();
+                $('input[name=trade_scene_remark]').removeAttr('lay-verify').removeAttr('lay-reqText');
+            }
+        }
+        checkTradeSceneRemark();
+
+        form.on('select(trade_scene)', function () {
+            checkTradeSceneRemark();
+            form.render('select');
+        });
+
+        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 = 'price_adjust';
+                    data.field.action_id  = e.data.return_id;
+                    oaCheck.submit(data.field, checkCallback);
+                }
+            };
+            let clickbtn = $(this);
+            tool.post('/finance/priceadjust/add', data.field, callback, clickbtn);
+            return false;
+        });
+    }
+</script>
+{/block}

+ 104 - 0
app/finance/view/priceadjust/datalist.html

@@ -0,0 +1,104 @@
+{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="搜索客户ID">
+                </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/priceadjust/add">新建调价申请
+                    </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_priceadjust" lay-filter="table_priceadjust"></table>
+        </div>
+    </div>
+</div>
+{/block}
+
+{block name="script"}
+<script>
+    const moduleInit = ['tool', 'oaTable'];
+    function gouguInit() {
+        var tool = layui.tool, table = layui.table, form = layui.form;
+
+        var tab = 0;
+        var tableIns = table.render({
+            elem: '#table_priceadjust',
+            url: '/finance/priceadjust/datalist',
+            where: { tab: tab },
+            page: true,
+            cols: [[
+                { field: 'id', title: 'ID', width: 80 },
+                { field: 'customer_id', title: '客户ID', minWidth: 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) {
+                        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>';
+                    }
+                }
+            ]],
+            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();
+                        });
+                    });
+                });
+            }
+        });
+
+        // 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}

+ 97 - 0
app/finance/view/priceadjust/view.html

@@ -0,0 +1,97 @@
+{extend name="../../base/view/common/base" /}
+{block name="body"}
+<div class="p-page">
+    <h3 class="pb-2">调价申请详情</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">客户ID</td>
+            <td colspan="3">{$detail.customer_id|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">历史费用是否需要补扣</td>
+            <td colspan="3">{:price_adjust_yn_name($detail['history_fee_deduct'])}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">客户交易主要场景</td>
+            <td colspan="3">{$detail.trade_scene_name|default=''}</td>
+        </tr>
+        {notempty name="$detail.trade_scene_remark"}
+        <tr>
+            <td class="layui-td-gray-2">交易场景备注</td>
+            <td colspan="3">{$detail.trade_scene_remark}</td>
+        </tr>
+        {/notempty}
+        <tr>
+            <td class="layui-td-gray-2">卡BIN</td>
+            <td>{$detail.card_bin_name|default=''}</td>
+            <td class="layui-td-gray">卡BIN备注</td>
+            <td>{$detail.card_bin_remark|default='-'}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">卡类型</td>
+            <td colspan="3">{$detail.card_type_name|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">调整的费项【充值】</td>
+            <td colspan="3">{$detail.fee_recharge|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">调整的费项【开卡】</td>
+            <td colspan="3">{$detail.fee_card_open|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">调整的其他费项</td>
+            <td colspan="3">{$detail.fee_other|default=''}</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">客户月均整体交易量</td>
+            <td>{$detail.monthly_trade_vol|default=''} 万美金</td>
+            <td class="layui-td-gray">预估切换交易量</td>
+            <td>{$detail.estimated_trade_vol|default=''} 万美金</td>
+        </tr>
+        <tr>
+            <td class="layui-td-gray-2">友商名称和报价</td>
+            <td colspan="3">{$detail.competitor_info|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: 'price_adjust' });
+    }
+</script>
+{/block}

+ 144 - 0
app/finance/view/priceadjustcate/datalist.html

@@ -0,0 +1,144 @@
+{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>
+            <li data-type="2">卡BIN</li>
+            <li data-type="3">卡类型</li>
+        </ul>
+    </div>
+    <table class="layui-hide" id="table_price_adjust_cate" lay-filter="table_price_adjust_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_price_adjust_cate',
+            toolbar: '#toolbarDemo',
+            url: '/finance/priceadjustcate/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_price_adjust_cate)', function (obj) {
+            if (obj.event === 'edit') {
+                editCate(obj.data);
+            }
+            if (obj.event === 'disable') {
+                layer.confirm('确定要禁用该记录吗?', { icon: 3 }, function (index) {
+                    tool.post('/finance/priceadjustcate/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/priceadjustcate/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/priceadjustcate/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; }
+                    var tool = layui.tool;
+                    tool.post('/finance/priceadjustcate/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}

+ 1 - 0
app/home/common.php

@@ -92,3 +92,4 @@ function admin_module()
     $group = Db::name('AdminModule')->order('id asc')->select()->toArray();
     return $group;
 }
+

+ 2 - 1
composer.json

@@ -36,7 +36,8 @@
         "phpoffice/phpspreadsheet": "^1.2",
         "phpoffice/phpword": "^1.2",
 		"mpdf/mpdf": "^8.1",
-		"alibabacloud/dysmsapi-20170525":"^4.3"
+		"alibabacloud/dysmsapi-20170525":"^4.3",
+        "topthink/think-migration": "^3.1"
     },
     "require-dev": {
         "symfony/var-dumper": "^6.0",

+ 52 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "fdf3f59aa81d0d137889debd300186fc",
+    "content-hash": "311aea73cd6d85308790aa772d0cdf43",
     "packages": [
         {
             "name": "adbario/php-dot-notation",
@@ -2433,6 +2433,57 @@
             "time": "2024-08-07T10:06:35+00:00"
         },
         {
+            "name": "topthink/think-migration",
+            "version": "v3.1.1",
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.tencent.com/repository/composer/topthink/think-migration/v3.1.1/topthink-think-migration-v3.1.1.zip",
+                "reference": "22c44058e1454f3af1d346e7f6524fbe654de7fb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2",
+                "topthink/framework": "^6.0 || ^8.0",
+                "topthink/think-helper": "^3.0.3"
+            },
+            "require-dev": {
+                "composer/composer": "^2.5.8",
+                "fzaninotto/faker": "^1.8",
+                "robmorgan/phinx": "^0.13.4"
+            },
+            "suggest": {
+                "fzaninotto/faker": "Required to use the factory builder (^1.8)."
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\migration\\Service"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Phinx\\": "phinx",
+                    "think\\migration\\": "src"
+                }
+            },
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "support": {
+                "issues": "https://github.com/top-think/think-migration/issues",
+                "source": "https://github.com/top-think/think-migration/tree/v3.1.1"
+            },
+            "time": "2023-09-14T05:51:31+00:00"
+        },
+        {
             "name": "topthink/think-multi-app",
             "version": "v1.1.1",
             "dist": {

+ 9 - 9
config/database.php

@@ -14,25 +14,25 @@ return [
     'connections'     => [
         'mysql' => [
             // 数据库类型
-            'type'               =>  'mysql',
+            'type'               =>  env('database.type', 'mysql'),
             // 服务器地址
-            'hostname'           =>  '127.0.0.1',
+            'hostname'           =>  env('database.hostname', '127.0.0.1'),
             // 数据库名
-            'database'           =>  'oa',
+            'database'           =>  env('database.database', 'oa'),
             // 用户名
-            'username'           =>  'root',
+            'username'           =>  env('database.username', 'root'),
             // 密码
-            'password'           =>  '123456',
+            'password'           =>  env('database.password', 'root'),
             // 端口
-            'hostport'           =>  '3306',
+            'hostport'           =>  env('database.hostport', '3306'),
             // 数据库表前缀
-            'prefix'             =>  'oa_',
+            'prefix'             =>  env('database.prefix', 'oa_'),
             // 数据库连接参数
             'params'          => [],
             // 数据库编码默认采用utf8mb4
-            'charset'         => 'utf8mb4',
+            'charset'         => env('database.charset', 'utf8mb4'),
             // 数据库调试模式
-            'debug'           => false,
+            'debug'           => env('database.debug', false),
             // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
             'deploy'          => 0,
             // 数据库读写是否分离 主从式有效

+ 51 - 0
database/migrations/20260525033651_create_price_adjust.php

@@ -0,0 +1,51 @@
+<?php
+
+use Phinx\Db\Adapter\MysqlAdapter;
+use think\migration\Migrator;
+
+class CreatePriceAdjust extends Migrator
+{
+    public function change(): void {
+        $table = $this->table('price_adjust', [
+            'id'          => 'id',
+            'engine'      => 'InnoDB',
+            'collation'   => 'utf8mb4_general_ci',
+            'comment'     => '调价申请表',
+            'auto_id'     => true,
+        ]);
+
+        $table
+            // 业务字段
+            ->addColumn('customer_id', 'string', ['limit' => 100, 'null' => false, 'default' => '', 'comment' => '客户ID'])
+            ->addColumn('history_fee_deduct', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '历史费用是否需要补扣:0否,1是'])
+            ->addColumn('trade_scene', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '客户交易主要场景:1广告服务类,2平台电商类,3钱包类,4订阅类,5游戏类,6航空机旅,7留学缴费,8SAAS,9其他'])
+            ->addColumn('trade_scene_remark', 'text', ['null' => true, 'comment' => '交易场景备注(场景为其他时必填)'])
+            ->addColumn('card_bin', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '卡BIN:1投流组,2电商组,3Q卡,4A卡,5V卡'])
+            ->addColumn('card_bin_remark', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '卡BIN备注'])
+            ->addColumn('card_type', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '卡类型:1储值卡,2共享卡'])
+            ->addColumn('fee_recharge', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '调整的费项【充值】'])
+            ->addColumn('fee_card_open', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '调整的费项【开卡】'])
+            ->addColumn('fee_other', 'string', ['limit' => 500, 'null' => false, 'default' => '', 'comment' => '调整的其他费项'])
+            ->addColumn('monthly_trade_vol', 'string', ['limit' => 100, 'null' => false, 'default' => '', 'comment' => '客户月均整体交易量(万美金)'])
+            ->addColumn('estimated_trade_vol', 'string', ['limit' => 100, 'null' => false, 'default' => '', 'comment' => '预估切换交易量(万美金)'])
+            ->addColumn('competitor_info', 'text', ['null' => true, '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();
+    }
+}

+ 27 - 0
database/migrations/20260525061907_create_price_adjust_cate.php

@@ -0,0 +1,27 @@
+<?php
+
+use Phinx\Db\Adapter\MysqlAdapter;
+use think\migration\Migrator;
+use think\migration\db\Column;
+
+class CreatePriceAdjustCate extends Migrator
+{
+    public function change(): void {
+        $table = $this->table('price_adjust_cate', [
+            'engine'    => 'InnoDB',
+            'collation' => 'utf8mb4_general_ci',
+            'comment'   => '调价申请类型字典表',
+        ]);
+
+        $table
+            ->addColumn('type', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'null' => false, 'default' => 0, 'comment' => '类型:1客户交易场景,2卡BIN'])
+            ->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();
+    }
+}

+ 51 - 0
database/migrations/20260525061908_seed_price_adjust_cate_data.php

@@ -0,0 +1,51 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedPriceAdjustCateData extends Migrator
+{
+    public function up(): void
+    {
+        $now = time();
+        foreach ($this->data() as [$type, $title, $value, $isOther, $sort]) {
+            $this->execute("INSERT INTO `oa_price_adjust_cate`
+                (`type`, `title`, `value`, `is_other`, `sort`, `status`, `create_time`, `delete_time`)
+                VALUES ({$type}, '{$title}', {$value}, {$isOther}, {$sort}, 1, {$now}, 0)");
+        }
+    }
+
+    public function down(): void
+    {
+        foreach ($this->data() as [$type, $title]) {
+            $this->execute("DELETE FROM `oa_price_adjust_cate` WHERE `type` = {$type} AND `title` = '{$title}'");
+        }
+    }
+
+    /**
+     * @return array<int, array{0:int,1:string,2:int,3:int,4:int}> [type, title, value, is_other, sort]
+     */
+    protected function data(): array
+    {
+        return [
+            // 客户交易主要场景(type=1)
+            [1, '广告服务类:Facebook、Google、Tiktok等', 1, 0, 1],
+            [1, '平台电商类:速卖通、Ebay、亚马逊等', 2, 0, 2],
+            [1, '钱包类:PayPal消费类、WeChatPay、wise等', 3, 0, 3],
+            [1, '订阅类:娱乐服务', 4, 0, 4],
+            [1, '游戏类', 5, 0, 5],
+            [1, '航空机旅', 6, 0, 6],
+            [1, '留学缴费', 7, 0, 7],
+            [1, 'SAAS:云', 8, 0, 8],
+            [1, '其他-需在备注中说明', 9, 1, 9],
+            // 卡BIN(type=2)
+            [2, '投流组(G/A/Q/P)', 1, 0, 1],
+            [2, '电商组(D/P/Q/G)', 2, 0, 2],
+            [2, 'Q卡', 3, 0, 3],
+            [2, 'A卡', 4, 0, 4],
+            [2, 'V卡', 5, 0, 5],
+            // 卡类型(type=3)
+            [3, '储值卡', 1, 0, 1],
+            [3, '共享卡', 2, 0, 2],
+        ];
+    }
+}

+ 151 - 0
database/migrations/20260525061909_seed_price_adjust_data.php

@@ -0,0 +1,151 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedPriceAdjustData 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
+            ('调价申请', 'price_adjust', {$flowModuleId}, 'price_adjust', 'icon-tiaojia',
+             '', 50, 1, 1, 0, 1, 0,
+             1, '/finance/priceadjust/add', '/finance/priceadjust/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
+            ('调价申请审批', {$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
+            ('调价申请审批', 'price_adjust', 2, 0, '', '/finance/priceadjust/view/id/{action_id}',
+             '{from_user}提交了一个『调价申请』,请及时审批',
+             '您有一个新的『调价申请』需要处理。',
+             '您提交的『调价申请』已被审批通过。',
+             '您在{create_time}提交的『调价申请』已于{date}被审批通过。',
+             '您提交的『调价申请』已被驳回拒绝。',
+             '您在{create_time}提交的『调价申请』已于{date}被驳回拒绝。',
+             '{from_user}提交的『调价申请』已被审批通过并抄送给你',
+             '{from_user}在{create_time}提交的『调价申请』已被审批通过并抄送给你,请及时查看详情。',
+             '', 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/priceadjust/datalist', '调价申请', '调价申请', 'finance', 'icon-tiaojia', 1, 50, 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/priceadjust/datalist', '列表',      '调价申请-列表',     'finance', '', 2, 1, 1, {$now}),
+            ({$ruleId}, 'finance/priceadjust/add',      '添加/编辑', '调价申请-添加编辑', 'finance', '', 2, 2, 1, {$now}),
+            ({$ruleId}, 'finance/priceadjust/view',     '详情',      '调价申请-详情',     'finance', '', 2, 3, 1, {$now}),
+            ({$ruleId}, 'finance/priceadjust/del',      '删除',      '调价申请-删除',     '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/priceadjust/%'")
+                      ->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='price_adjust'")
+                      ->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` = 'price_adjust'");
+        $this->execute("DELETE FROM `oa_flow_cate` WHERE `name` = 'price_adjust'");
+    }
+
+    /**
+     * 从所有角色的 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']}");
+            }
+        }
+    }
+}

+ 87 - 0
database/migrations/20260525061910_seed_price_adjust_cate_rule.php

@@ -0,0 +1,87 @@
+<?php
+
+use think\migration\Migrator;
+
+class SeedPriceAdjustCateRule 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/priceadjustcate/datalist', '调价类型', '调价类型管理', 'finance', '', 1, 51, 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/priceadjustcate/datalist', '列表',      '调价类型-列表',     'finance', '', 2, 1, 1, {$now}),
+            ({$ruleId}, 'finance/priceadjustcate/add',      '添加/编辑', '调价类型-添加编辑', 'finance', '', 2, 2, 1, {$now}),
+            ({$ruleId}, 'finance/priceadjustcate/del',      '删除',      '调价类型-删除',     'finance', '', 2, 3, 1, {$now}),
+            ({$ruleId}, 'finance/priceadjustcate/set',      '设置',      '调价类型-设置',     '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/priceadjustcate/%'")
+                      ->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']}");
+            }
+        }
+    }
+}