初始化提交

This commit is contained in:
maticarmy
2025-02-10 10:39:00 +08:00
commit 59cd2c19d1
491 changed files with 54545 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
/**
* 后台管理基础控制器
*/
class Base extends Controller
{
/**
* 初始化方法
*/
protected function initialize()
{
parent::initialize();
// 直接使用 admin 的权限验证
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
class Error extends Base
{
public function forbidden()
{
return $this->fetch();
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use app\manager\sys\RedisManager;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 后台首页控制器
*/
class Index extends Controller
{
/**
* 显示后台首页
*/
public function index()
{
$userid = AdminService::getUserId();
// 获取实例
$redis = RedisManager::getInstance();
$this->assign("userid", $userid);
$this->fetch();
}
}

View File

@@ -0,0 +1,216 @@
<?php
declare (strict_types = 1);
namespace app\manager\controller;
use app\manager\model\Member as MemberModel;
use think\admin\Controller;
use think\facade\Db;
/**
* 会员管理
* @auth true
* @menu true
*/
class Member extends Controller
{
/**
* 会员列表
* @auth true
* @menu true
*/
public function index()
{
$this->title = '会员管理';
if ($this->request->isGet() && $this->request->get('output') === 'layui.table') {
$where = [];
// 搜索条件
$keyword = input('keyword', '', 'trim');
if (!empty($keyword)) {
$where[] = ['email|order_id', 'like', "%{$keyword}%"];
}
// 状态筛选
$status = input('status', '', 'trim');
if ($status !== '') {
$where[] = ['status', '=', intval($status)];
}
// 查询数据
$query = MemberModel::where($where)->order('id desc');
$result = $query->paginate([
'list_rows' => input('limit', 20),
'page' => input('page', 1),
]);
return json([
'code' => 0,
'msg' => '',
'count' => $result->total(),
'data' => $result->items()
]);
}
return $this->fetch();
}
/**
* 添加会员
* @auth true
*/
public function add()
{
if ($this->request->isPost()) {
$data = $this->_vali([
'email.require' => '邮箱不能为空!',
'email.email' => '邮箱格式错误!',
'order_id.require' => '闲鱼订单号不能为空!',
'expire_time.require' => '有效期不能为空!',
'usage_limit.require' => '可用次数不能为空!',
'usage_limit.number' => '可用次数必须为数字!',
]);
// 检查唯一性
$map = [
['email', '=', $data['email']]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('邮箱已存在!');
}
$map = [
['order_id', '=', $data['order_id']]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('闲鱼订单号已存在!');
}
// 设置默认值
$data['status'] = isset($data['status']) ? 1 : 0;
$data['used_count'] = 0;
// 保存数据
if (MemberModel::create($data) !== false) {
$this->success('添加成功!');
} else {
$this->error('添加失败!');
}
}
$this->title = '添加会员';
return $this->fetch('form');
}
/**
* 编辑会员
* @auth true
*/
public function edit()
{
$id = input('id');
if (empty($id)) $this->error('参数错误!');
if ($this->request->isPost()) {
$data = $this->_vali([
'id.require' => 'ID不能为空',
'email.require' => '邮箱不能为空!',
'email.email' => '邮箱格式错误!',
'order_id.require' => '闲鱼订单号不能为空!',
'expire_time.require' => '有效期不能为空!',
'usage_limit.require' => '可用次数不能为空!',
'usage_limit.number' => '可用次数必须为数字!',
]);
// 检查唯一性
$map = [
['email', '=', $data['email']],
['id', '<>', $id]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('邮箱已存在!');
}
$map = [
['order_id', '=', $data['order_id']],
['id', '<>', $id]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('闲鱼订单号已存在!');
}
// 设置默认值
$data['status'] = isset($data['status']) ? 1 : 0;
$data['used_count'] = isset($data['used_count']) ? intval($data['used_count']) : 0;
// 保存数据
if (MemberModel::update($data) !== false) {
$this->success('编辑成功!');
} else {
$this->error('编辑失败!');
}
}
$this->title = '编辑会员';
$this->member = MemberModel::find($id);
return $this->fetch('form');
}
/**
* 修改状态
* @auth true
*/
public function state()
{
MemberModel::mSave($this->_vali([
'status.in:0,1' => '状态值范围异常!',
'status.require' => '状态值不能为空!',
]));
}
/**
* 删除会员
* @auth true
*/
public function remove()
{
MemberModel::mDelete();
}
/**
* 表单数据处理
* @param array $data
*/
protected function _form_filter(array &$data)
{
if ($this->request->isPost()) {
// 检查数据完整性
empty($data['username']) && $this->error('用户名不能为空!');
empty($data['order_id']) && $this->error('闲鱼订单号不能为空!');
empty($data['expire_time']) && $this->error('有效期不能为空!');
// 检查唯一性
$map = [];
if (empty($data['id'])) {
$map[] = ['username', '=', $data['username']];
$map[] = ['order_id', '=', $data['order_id']];
if (MemberModel::where($map)->count() > 0) {
$this->error('用户名或闲鱼订单号已存在!');
}
} else {
$map[] = ['id', '<>', $data['id']];
$map[] = ['username|order_id', '=', $data['username']];
if (MemberModel::where($map)->count() > 0) {
$this->error('用户名或闲鱼订单号已存在!');
}
}
// 设置默认值
$data['status'] = isset($data['status']) ? 1 : 0;
$data['used_count'] = isset($data['used_count']) ? intval($data['used_count']) : 0;
$data['balance'] = isset($data['balance']) ? floatval($data['balance']) : 0;
$data['points'] = isset($data['points']) ? intval($data['points']) : 0;
$data['level'] = isset($data['level']) ? intval($data['level']) : 1;
}
}
}

View File

@@ -0,0 +1,503 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 包名管理
* @auth true # 表示需要验证权限
* @menu true # 表示可以生成菜单
*/
class Package extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package';
/**
* 检查包名权限
* @param int $packageId 包名ID
* @return bool
*/
protected function checkPackageAuth($packageId)
{
// 超级管理员直接放行
if (AdminService::instance()->isSuper()) {
return true;
}
// 检查用户是否有包名管理模块的权限
if (!AdminService::instance()->check('package/index')) {
return false;
}
// 检查具体包名权限
$userId = AdminService::instance()->getUserId();
$exists = $this->app->db->name('offer_package_auth')
->where(['package_id' => $packageId, 'user_id' => $userId])
->find();
return !empty($exists);
}
/**
* 获取当前用户可访问的包名ID列表
*/
protected function getAuthPackageIds()
{
// 超级管理员可以访问所有
if (AdminService::instance()->isSuper()) {
return [];
}
// 如果没有包名管理模块权限,则不能访问任何包名
if (!AdminService::instance()->check('package/index')) {
return [-1];
}
// 获取用户被授权的包名列表
$userId = AdminService::instance()->getUserId();
$packageIds = $this->app->db->name('offer_package_auth')
->where(['user_id' => $userId])
->column('package_id');
return $packageIds ?: [-1];
}
/**
* 包名列表
* @auth true # 需要验证权限
* @menu true # 菜单可见
*/
public function index()
{
if ($this->request->isPost()) {
try {
// 获取请求参数
$page = $this->request->post('page/d', 1);
$limit = $this->request->post('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = $this->request->post('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 应用名称搜索
if ($name = $this->request->post('name/s', '')) {
$where[] = ['name', 'like', "%{$name}%"];
}
// 状态筛选
if ($status = $this->request->post('status/s', '')) {
$where[] = ['status', '=', $status];
}
// 创建人搜索
if ($username = $this->request->post('username/s', '')) {
$where[] = ['username', 'like', "%{$username}%"];
}
// 时间范围处理
if ($add_time = $this->request->post('add_time/s', '')) {
$times = explode(' - ', $add_time);
if (count($times) == 2) {
$start_time = strtotime($times[0] . ' 00:00:00');
$end_time = strtotime($times[1] . ' 23:59:59');
$where[] = ['add_time', 'between', [$start_time, $end_time]];
}
}
// 查询数据
$query = $this->app->db->name('offer_package')->alias('p');
// 非超级管理员需要进行权限过滤
if (!AdminService::instance()->isSuper()) {
// 检查包名管理模块权限
if (!AdminService::instance()->check('package/index')) {
return json([
'code' => 0,
'msg' => '',
'count' => 0,
'data' => []
]);
}
$userId = AdminService::instance()->getUserId();
// 通过左连接查询有权限的包名
$query->leftJoin('offer_package_auth pa', 'p.id = pa.package_id')
->where('pa.user_id', $userId);
}
// 先获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->field('p.*') // 只查询包名表的字段
->order('p.id desc')
->group('p.id') // 防止重复数据
->page($page, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取包名列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
$this->fetch();
}
/**
* 添加包名
* @auth true
* @menu true
*/
public function add()
{
if ($this->request->isPost()) {
// 开启事务
$this->app->db->startTrans();
try {
$data = $this->_vali([
'package_name.require' => '包名不能为空!',
'name.require' => '应用名称不能为空!',
'status.require' => '状态不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查包名是否已存在
$exists = $this->app->db->name('offer_package')
->where(['package_name' => $data['package_name']])
->find();
if ($exists) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '包名已存在,请更换!']);
}
// 添加时间字段
$now = time();
$data['add_time'] = $now;
$data['update_time'] = $now;
$data['username'] = session('user.username') ?? '';
// 插入包名数据
$result = $this->app->db->name('offer_package')->insertGetId($data);
if ($result === false) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '添加失败,请重试!']);
}
// 非超级管理员需要添加权限记录
if (!AdminService::instance()->isSuper()) {
$authData = [
'package_id' => $result,
'user_id' => AdminService::instance()->getUserId(),
'create_at' => date('Y-m-d H:i:s')
];
$authResult = $this->app->db->name('offer_package_auth')->insert($authData);
if ($authResult === false) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '添加权限记录失败!']);
}
}
$this->app->db->commit();
sysoplog('积分墙列表', '添加成功');
return json(['code' => 1, 'info' => '添加成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
trace("添加包名异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 编辑包名
* @auth true
* @menu true
*/
public function edit()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '记录ID不能为空',
'package_name.require' => '包名不能为空!',
'name.require' => '应用名称不能为空!',
'status.require' => '状态不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查包名是否已存在(排除当前记录)
$exists = $this->app->db->name('offer_package')
->where('package_name', $data['package_name'])
->where('id', '<>', $data['id'])
->find();
if ($exists) {
return json(['code' => 0, 'info' => '包名已存在,请更换!']);
}
// 更新时间
$data['update_time'] = time();
$data['username'] = session('user.username') ?? '';
$result = $this->app->db->name('offer_package')
->where(['id' => $data['id']])
->update($data);
if ($result === false) {
return json(['code' => 0, 'info' => '更新失败,请重试!']);
}
sysoplog('积分墙列表', '更新成功');
return json(['code' => 1, 'info' => '更新成功!']);
} catch (\Exception $e) {
trace("编辑包名异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 删除包名
* @auth true
* @menu true
*/
public function remove()
{
if ($this->request->isPost()) {
// 开启事务
$this->app->db->startTrans();
try {
$id = $this->request->param('id');
if (empty($id)) {
return json(['code' => 0, 'info' => '参数错误,请刷新重试!']);
}
// 检查权限
if (!$this->checkPackageAuth($id)) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 删除包名记录
$result = $this->app->db->name('offer_package')
->where(['id' => $id])
->delete();
if ($result === false) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '删除失败,请重试!']);
}
// 同步删除权限表中的相关记录
$this->app->db->name('offer_package_auth')
->where(['package_id' => $id])
->delete();
$this->app->db->commit();
sysoplog('积分墙列表', '删除成功');
return json(['code' => 1, 'info' => '删除成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
trace("删除包名异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 切换状态
* @auth true
* @menu true
*/
public function state()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '记录ID不能为空',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查记录是否存在
$exists = $this->app->db->name('offer_package')
->where(['id' => $data['id']])
->find();
if (empty($exists)) {
return json(['code' => 0, 'info' => '记录不存在!']);
}
// 更新状态
$updateData = [
'status' => $data['status'],
'update_time' => time(),
'username' => session('user.username') ?? ''
];
$result = $this->app->db->name('offer_package')
->where(['id' => $data['id']])
->update($updateData);
if ($result === false) {
return json(['code' => 0, 'info' => '状态更新失败,请重试!']);
}
sysoplog('积分墙列表', '状态更新成功');
return json(['code' => 1, 'info' => '状态更新成功!']);
} catch (\Exception $e) {
trace("状态切换异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 生成测试数据
* @auth true
*/
public function generateTestData()
{
try {
// 设置执行时间为无限制
set_time_limit(0);
ini_set('memory_limit', '1024M');
// 开启事务
$this->app->db->startTrans();
try {
$insertData = [];
$existPackages = [];
$total = 8000; // 总记录数
$batchSize = 500; // 每批插入数量
$startTime = time();
// 获取已存在的包名,避免重复
$exists = $this->app->db->name($this->table)
->column('package_name');
$existPackages = array_flip($exists);
// 预生成一些常用词,提高应用名称的真实性
$prefixes = ['Super', 'My', 'Fast', 'Smart', 'Easy', 'Pro', 'Best', 'Quick'];
$suffixes = ['Browser', 'Player', 'Cleaner', 'Manager', 'Tool', 'Helper', 'App', 'Game'];
$domains = ['com', 'net', 'org', 'app', 'tech', 'io'];
// 生成记录
for ($i = 0; $i < $total; $i++) {
// 生成唯一包名
do {
// 生成更真实的包名 xxx.xxx.xxx.xxx
$domain = $domains[array_rand($domains)];
$company = $this->generateRandomString(rand(3, 8));
$product = $this->generateRandomString(rand(3, 8));
$module = $this->generateRandomString(rand(3, 8));
$packageName = "{$domain}.{$company}.{$product}.{$module}";
} while (isset($existPackages[$packageName]));
$existPackages[$packageName] = 1;
// 生成更真实的应用名称
$prefix = $prefixes[array_rand($prefixes)];
$suffix = $suffixes[array_rand($suffixes)];
$appName = $prefix . ' ' . $suffix . ' ' . ($i + 1);
// 生成数据
$insertData[] = [
'package_name' => $packageName,
'name' => $appName,
'status' => rand(0, 1), // 随机状态
'add_time' => time() - rand(0, 86400 * 30), // 随机时间最近30天内
'update_time' => time(),
'username' => session('user.username') ?? 'system'
];
// 批量插入
if (count($insertData) >= $batchSize) {
$this->app->db->name($this->table)->insertAll($insertData);
$insertData = [];
// 输出进度
$progress = round(($i + 1) / $total * 100, 2);
trace("数据生成进度:{$progress}%");
}
}
// 插入剩余数据
if (!empty($insertData)) {
$this->app->db->name($this->table)->insertAll($insertData);
}
$endTime = time();
$timeUsed = $endTime - $startTime;
$this->app->db->commit();
return json([
'code' => 1,
'info' => "测试数据生成成功!共生成 {$total} 条记录,耗时 {$timeUsed}"
]);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("生成测试数据异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常:' . $e->getMessage()]);
}
}
/**
* 生成随机字符串
* @param int $length 长度
* @return string
*/
private function generateRandomString($length = 5)
{
// 添加数字,使包名更真实
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
$result = '';
// 第一个字符必须是字母
$result .= $characters[rand(0, 25)];
// 生成剩余字符
for ($i = 1; $i < $length; $i++) {
$result .= $characters[rand(0, strlen($characters) - 1)];
}
return $result;
}
}

View File

@@ -0,0 +1,918 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
class PackageAuth extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_auth';
/**
* 包名权限列表
* @auth true
* @menu true
*/
public function index()
{
if ($this->request->isGet()) {
// 获取所有管理员(排除超级管理员)
$users = $this->app->db->name('system_user')
->where([
['status', '=', 1], // 状态正常的用户
['authorize', 'not like', '%admin%'], // 排除admin权限组的用户
['id', '<>', '10000'] // 排除系统超管账号
])
->field('id,username,authorize')
->order('id asc')
->select()
->filter(function($user) {
// 只显示有包名管理权限的用户
return AdminService::instance()->check('package/index', $user['id']);
})
->toArray();
// 获取现有权限配置
$auths = $this->app->db->name($this->table)
->select()
->toArray();
// 整理权限数据
$authMap = [];
foreach ($auths as $auth) {
$authMap[$auth['user_id']][] = $auth['package_id'];
}
$this->assign([
'users' => $users,
'authMap' => $authMap
]);
$this->fetch();
}
}
/**
* 获取包名列表(分页)
* @auth true
*/
public function getPackageList()
{
if ($this->request->isPost()) {
try {
$page = $this->request->post('page/d', 1);
$limit = $this->request->post('limit/d', 20);
$keyword = $this->request->post('keyword/s', '');
$startTime = $this->request->post('start_time/s', '');
$endTime = $this->request->post('end_time/s', '');
$where = [];
// 只显示启用的包名
$where[] = ['status', '=', 1];
if ($keyword !== '') {
$where[] = ['package_name|name', 'like', "%{$keyword}%"];
}
// 修改时间筛选条件处理方式
if ($startTime && $endTime) {
$where[] = ['add_time', 'between', [
strtotime($startTime),
strtotime($endTime)
]];
}
// 使用正确的表名
$query = $this->app->db->name('offer_package');
// 先获取总数
$total = $query->where($where)->count();
// 获取列表数据
$list = $query->where($where)
->field('id,package_name,name,add_time,update_time,status') // 修改字段名
->order('id desc')
->page($page, $limit)
->select()
->toArray();
// 格式化时间戳
foreach ($list as &$item) {
$item['add_time'] = date('Y-m-d H:i:s', intval($item['add_time']));
$item['update_time'] = date('Y-m-d H:i:s', intval($item['update_time']));
}
// 记录调试信息
trace("包名列表查询:" . json_encode([
'where' => $where,
'page' => $page,
'limit' => $limit,
'total' => $total,
'list_count' => count($list),
'sql' => $query->getLastSql()
], JSON_UNESCAPED_UNICODE));
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
// 详细记录异常信息
trace("获取包名列表异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']);
}
}
}
/**
* 获取用户包名权限
* @auth true
* @api true
*/
public function getUserAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
if (empty($userId)) {
return json(['code' => 0, 'info' => '请选择管理员!']);
}
// 只获取包名ID列表
$packageIds = $this->app->db->name($this->table)
->where('user_id', $userId)
->column('package_id'); // 直接返回ID数组
return json([
'code' => 1,
'info' => '获取成功!',
'data' => $packageIds // 直接返回ID数组供 getPackageDetails 使用
]);
} catch (\Exception $e) {
trace("获取用户权限异常:{$e->getMessage()}\n" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 批量授权
* @auth true
*/
public function batchAuth()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'user_ids.require' => '请选择管理员!',
'package_ids.require' => '请选择包名!'
]);
// 验证所选用户是否都有包名管理权限
foreach ($data['user_ids'] as $userId) {
if (!AdminService::instance()->check('package/index', $userId)) {
return json(['code' => 0, 'info' => '选中的用户中有人没有包名管理权限!']);
}
}
// 开启事务
$this->app->db->startTrans();
try {
foreach ($data['user_ids'] as $userId) {
// 删除原有权限
$this->app->db->name($this->table)
->where('user_id', $userId)
->delete();
// 添加新权限
$insertData = [];
foreach ($data['package_ids'] as $packageId) {
$insertData[] = [
'user_id' => $userId,
'package_id' => $packageId,
'create_at' => date('Y-m-d H:i:s')
];
}
if (!empty($insertData)) {
$this->app->db->name($this->table)->insertAll($insertData);
}
}
$this->app->db->commit();
sysoplog('积分墙权限', '批量授权成功');
return json(['code' => 1, 'info' => '批量授权成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("批量授权异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常请稍后重<E5908E><E9878D><EFBFBD>']);
}
}
}
/**
* 清空
* @auth true
*/
public function clearAuth()
{
if ($this->request->isPost()) {
try {
$post = $this->request->post();
// 验证用户ID
if (empty($post['user_ids']) || !is_array($post['user_ids'])) {
return json(['code' => 0, 'info' => '选择管理员!']);
}
// 开启事务
$this->app->db->startTrans();
try {
foreach ($post['user_ids'] as $userId) {
// 删除用户的所有权限
$this->app->db->name($this->table)
->where('user_id', $userId)
->delete();
}
$this->app->db->commit();
sysoplog('积分墙权限', '权限清空成功');
return json(['code' => 1, 'info' => '权限清空成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
trace('清空权限事务异常:' . $e->getMessage());
throw $e;
}
} catch (\Exception $e) {
trace("清空权限异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 获取包名详情
*/
public function getPackageDetails()
{
if ($this->request->isPost()) {
try {
$packageIds = $this->request->post('package_ids/a', []);
// 记录请求参数
trace("getPackageDetails 请求参数:" . json_encode([
'package_ids' => $packageIds
], JSON_UNESCAPED_UNICODE));
// 如果没有包名ID返回空数组
if (empty($packageIds)) {
trace("getPackageDetails包名ID为空");
return json([
'code' => 1,
'info' => '获取成功!',
'data' => []
]);
}
// 获取包名详情
$packages = $this->app->db->name('offer_package')
->whereIn('id', $packageIds)
->field('id,package_name,name,status')
->select()
->toArray();
return json([
'code' => 1,
'info' => '获取成功!',
'data' => $packages
]);
} catch (\Exception $e) {
trace("getPackageDetails 异常:" . $e->getMessage());
trace("getPackageDetails 异常堆栈:" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!#3']);
}
}
}
/**
* 获取未授权的包名列表
* @auth true
*/
public function getUnauthorizedPackages()
{
// 同时支持 GET 和 POST 请求
$userId = $this->request->param('user_id/d', 0);
$keyword = $this->request->param('keyword/s', '');
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 10);
try {
if (empty($userId)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 获取已授权的包名ID
$authorizedIds = $this->app->db->name($this->table)
->where('user_id', $userId)
->column('package_id');
// 构建查询条件
$where = [];
// 排除已授权的包名
if (!empty($authorizedIds)) {
$where[] = ['id', 'not in', $authorizedIds];
}
// 搜索条件
if ($keyword !== '') {
$where[] = ['package_name|name', 'like', "%{$keyword}%"];
}
// 只显示启用的包名
$where[] = ['status', '=', 1];
// 查询数据
$query = $this->app->db->name('offer_package');
// 获取总数
$total = $query->where($where)->count();
// 获取列表
$list = $query->where($where)
->field('id,package_name,name,status')
->order('status desc,id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
// 记录调试信息
trace('未授权包名查询:' . json_encode([
'user_id' => $userId,
'keyword' => $keyword,
'where' => $where,
'sql' => $query->getLastSql(),
'total' => $total,
'list_count' => count($list)
], JSON_UNESCAPED_UNICODE));
return json([
'code' => 0, // layui table 要求成功码为 0
'msg' => '', // layui table 使用 msg 而不是 info
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取未授权包名异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']); // layui table 要求失败码为非 0
}
}
/**
* 批量添加授权
* @auth true
*/
public function addAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
$packageIds = $this->request->post('package_ids/a');
$isAll = $this->request->post('is_all/d', 0); // 是否全部授权
if (empty($userId)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 开启事务
$this->app->db->startTrans();
try {
if ($isAll) {
// 优化: 使用INSERT INTO SELECT语法直接插入
$sql = "INSERT INTO {$this->table} (user_id, package_id, create_at)
SELECT :user_id, id, :create_at
FROM offer_package
WHERE status = 1
AND id NOT IN (
SELECT package_id
FROM {$this->table}
WHERE user_id = :user_id2
)";
$this->app->db->execute($sql, [
'user_id' => $userId,
'user_id2' => $userId,
'create_at' => date('Y-m-d H:i:s')
]);
} else {
if (empty($packageIds)) {
return json(['code' => 0, 'info' => '请选择要授权的包名!']);
}
// 过滤掉已授权的包名
$existIds = $this->app->db->name($this->table)
->where('user_id', $userId)
->whereIn('package_id', $packageIds)
->column('package_id');
$newPackageIds = array_diff($packageIds, $existIds);
if (!empty($newPackageIds)) {
$insertData = array_map(function($packageId) use ($userId) {
return [
'user_id' => $userId,
'package_id' => $packageId,
'create_at' => date('Y-m-d H:i:s')
];
}, $newPackageIds);
$this->app->db->name($this->table)->insertAll($insertData);
}
}
$this->app->db->commit();
sysoplog('积分墙权限', '授权添加成功!');
return json(['code' => 1, 'info' => '授权添加成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("添加授权异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 移除单个包名权限
* @auth true
*/
public function removeAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
$packageId = $this->request->post('package_id/d', 0);
if (empty($userId) || empty($packageId)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
$result = $this->app->db->name($this->table)
->where([
'user_id' => $userId,
'package_id' => $packageId
])
->delete();
if ($result !== false) {
sysoplog('积分墙权限', '权限移除成功!');
return json(['code' => 1, 'info' => '权限移除成功!']);
} else {
return json(['code' => 0, 'info' => '权限移除失败!']);
}
} catch (\Exception $e) {
trace("移除权限异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 首页批量授权保存
* @auth true
*/
public function batchSaveAuth()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'user_ids.require' => '请选择管理员!',
'package_ids.require' => '请选择包名!'
]);
// 开启事务
$this->app->db->startTrans();
try {
foreach ($data['user_ids'] as $userId) {
// 删除原有权限
$this->app->db->name($this->table)
->where('user_id', $userId)
->delete();
// 添加新权限
$insertData = [];
foreach ($data['package_ids'] as $packageId) {
$insertData[] = [
'user_id' => $userId,
'package_id' => $packageId,
'create_at' => date('Y-m-d H:i:s')
];
}
if (!empty($insertData)) {
$this->app->db->name($this->table)->insertAll($insertData);
}
}
$this->app->db->commit();
sysoplog('积分墙权限', '批量授权成功!');
return json(['code' => 1, 'info' => '批量授权成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("首页批量授权异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 批量移除包名权限
* @auth true
*/
public function batchRemoveAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
$packageIds = $this->request->post('package_ids/a');
if (empty($userId) || empty($packageIds)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 开启事务
$this->app->db->startTrans();
try {
// 批量删除权限
$result = $this->app->db->name($this->table)
->where('user_id', $userId)
->whereIn('package_id', $packageIds)
->delete();
if ($result !== false) {
$this->app->db->commit();
sysoplog('积分墙权限', '批量移除成功!');
return json(['code' => 1, 'info' => '批量移除成功!']);
} else {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '批量移除失败!']);
}
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("批移除权限异:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 显示权限详情页面
* @auth true
*/
public function authDetail()
{
try {
$userId = input('user_id/d');
if (!$userId) {
$this->error('参数错误');
}
// 获取用户基本信息
$user = $this->app->db->name('system_user')
->where('id', $userId)
->field('id,username,status,create_at')
->find();
if (!$user) {
$this->error('用户不存在');
}
// 获取用户权限统计
$stats = [
'total' => $this->app->db->name($this->table)
->where('user_id', $userId)
->count(),
'active' => $this->app->db->name('offer_package')
->alias('p')
->join("{$this->table} a", 'p.id = a.package_id')
->where([
'a.user_id' => $userId,
'p.status' => 1
])
->count()
];
$this->assign([
'user' => $user,
'stats' => $stats,
'title' => '授权包名管理'
]);
return $this->fetch();
} catch (\Exception $e) {
$this->app->log->error("显示权限详情页面异常:{$e->getMessage()}");
$this->error('系统异常,请稍后重试!');
}
}
/**
* 获取权限详情数据
* @auth true
* @api true
*/
public function getAuthDetailData()
{
try {
$get = $this->_vali([
'user_id.require' => '用户ID不能为空',
'type.require' => '类型不能为空',
'page.default' => 1,
'limit.default' => 20,
'keyword.default' => '',
'sort.default' => 'id',
'order.default' => 'desc'
]);
// 确保分页参数为整数
$page = intval($get['page']);
$limit = intval($get['limit']);
// 构建基础查询
$query = $this->app->db->name('offer_package')
->alias('p');
if ($get['type'] === 'authorized') {
// 已授权包名查询
$query->join("{$this->table} a", 'p.id = a.package_id')
->where('a.user_id', $get['user_id']);
} else {
// 未授权包名查询
$query->whereNotExists(function($query) use ($get) {
$query->table($this->table)
->where('package_id=p.id')
->where('user_id', $get['user_id']);
})
->where('p.status', 1);
}
// 关键词搜索
if ($get['keyword'] !== '') {
$query->whereLike('p.package_name|p.name', "%{$get['keyword']}%");
}
// 获取总数
$total = $query->count();
// 获取列表数据
$list = $query->field([
'p.id',
'p.package_name',
'p.name',
'p.status',
$get['type'] === 'authorized' ? 'a.create_at' : 'p.add_time as create_at'
])
->order("{$get['sort']} {$get['order']}")
->page($page, $limit) // 使用转换后的整数值
->select()
->toArray();
// 格式化时间
foreach ($list as &$item) {
$item['create_at'] = format_datetime($item['create_at']);
$item['status_text'] = $item['status'] ? '启用' : '停用';
}
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取权限详情数据异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']);
}
}
/**
* 获取已授权的包名列表
* @auth true
*/
public function getAuthorizedPackages()
{
try {
$userId = $this->request->param('user_id/d', 0);
$keyword = $this->request->param('keyword/s', '');
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 10);
if (empty($userId)) {
return json(['code' => 1, 'msg' => '参数错误']);
}
// 优化1: 使用子查询优化 JOIN
$query = $this->app->db->name('offer_package')
->whereExists(function($query) use ($userId) {
$query->table($this->table)
->where('package_id=offer_package.id')
->where('user_id', $userId);
})
->where('status', 1);
// 优化2: 添加索引字段的索条件
if ($keyword !== '') {
$query->where(function($query) use ($keyword) {
$query->whereOr([
['package_name', 'like', "%{$keyword}%"],
['name', 'like', "%{$keyword}%"]
]);
});
}
// 优化3: 使用子查询获取总数,避免重复JOIN
$total = $query->count();
// 优化4: 只查询需要的字段
$list = $query->field([
'id',
'package_name',
'name',
'status',
"(SELECT create_at FROM {$this->table} WHERE package_id=offer_package.id AND user_id={$userId} LIMIT 1) as create_at"
])
->order('id desc')
->page($page, $limit)
->select()
->toArray();
// 优化5: 添加缓存
$cacheKey = "auth_packages_{$userId}_{$page}_{$limit}_" . md5($keyword);
cache($cacheKey, $list, 300); // 缓存5分钟
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取已授权包名列表异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']);
}
}
/**
* 获取授权包名列表(分页)
* @auth true
* @api true
*/
public function getAuthPackageList()
{
try {
$get = $this->_vali([
'user_id.require' => '用户ID不能为空',
'page.default' => 1,
'limit.default' => 10,
'keyword.default' => '',
'status.default' => 1,
'sort.default' => 'id',
'order.default' => 'desc'
]);
// 构建基础查询
$query = $this->app->db->name('offer_package')
->alias('p')
->join("{$this->table} a", 'p.id = a.package_id')
->where([
'a.user_id' => $get['user_id'],
'p.status' => $get['status']
]);
// 关键词搜索
if ($get['keyword'] !== '') {
$query->whereLike('p.package_name|p.name', "%{$get['keyword']}%");
}
// 获取总数
$total = $query->count();
// 获取分页数据
$list = $query->field([
'p.id',
'p.package_name',
'p.name',
'p.status',
'a.create_at',
'p.update_time'
])
->order("{$get['sort']} {$get['order']}")
->limit(($get['page'] - 1) * $get['limit'], $get['limit'])
->select()
->toArray();
// 格式化时间
foreach ($list as &$item) {
$item['create_at'] = format_datetime($item['create_at']);
$item['update_time'] = format_datetime($item['update_time']);
}
// 使用缓存
$cacheKey = "auth_package_list_{$get['user_id']}_{$get['page']}_{$get['limit']}_" . md5($get['keyword']);
cache($cacheKey, [
'total' => $total,
'list' => $list
], 300); // 缓存5分钟
return json([
'code' => 0,
'msg' => 'success',
'total' => $total,
'list' => $list,
'page' => $get['page'],
'limit' => $get['limit']
]);
} catch (\Exception $e) {
$this->app->log->error("获取授权包名列表异常:{$e->getMessage()}");
return json([
'code' => 1,
'msg' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 获取授权包名统计
* @auth true
* @api true
*/
public function getAuthPackageStats()
{
try {
$userId = input('user_id/d', 0);
if (empty($userId)) {
return json(['code' => 1, 'msg' => '用户ID不能为空']);
}
// 获取统计数据
$stats = [
'total' => $this->app->db->name($this->table)
->where('user_id', $userId)
->count(),
'active' => $this->app->db->name('offer_package')
->alias('p')
->join("{$this->table} a", 'p.id = a.package_id')
->where([
'a.user_id' => $userId,
'p.status' => 1
])
->count(),
'latest' => $this->app->db->name($this->table)
->where('user_id', $userId)
->order('create_at desc')
->value('create_at')
];
$stats['latest'] = $stats['latest'] ? format_datetime($stats['latest']) : '';
return json([
'code' => 0,
'msg' => 'success',
'data' => $stats
]);
} catch (\Exception $e) {
$this->app->log->error("获取授权包名统计异常:{$e->getMessage()}");
return json([
'code' => 1,
'msg' => '获取统计失败:' . $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,495 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 包名回传配置管理
* @auth true
* @menu true
*/
class PackageCallback extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_callback';
/**
* 回传配置列表
* @auth true
* @menu true
*/
public function index()
{
// 设置页面标题
$this->title = '回传配置管理';
$this->fetch();
}
/**
* 获取回传配置列表数据
* @auth true
*/
public function get_list()
{
try {
// 获取请求参数
$page = input('page/d', 1);
$limit = input('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = input('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 事件名称搜索
if ($event_name = input('event_name/s', '')) {
$where[] = ['event_name', 'like', "%{$event_name}%"];
}
// 状态筛选
if ($status = input('status/s', '')) {
$where[] = ['status', '=', $status];
}
// 非超级管理员需要进行权限过滤
if (!AdminService::instance()->isSuper()) {
// 检查包名管理模块权限
if (!AdminService::instance()->check('package/index')) {
return json([
'code' => 0,
'msg' => '',
'count' => 0,
'data' => []
]);
}
$userId = AdminService::instance()->getUserId();
// 获取有权限的包名ID列表
$packageIds = $this->app->db->name('offer_package_auth')
->where('user_id', $userId)
->column('package_id');
if (empty($packageIds)) {
return json([
'code' => 0,
'msg' => '',
'count' => 0,
'data' => []
]);
}
$where[] = ['package_id', 'in', $packageIds];
}
// 查询数据
$query = $this->app->db->name($this->table);
// 先获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->field('*')
->order('id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取回传配置列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
/**
* 添加回传配置
* @auth true
*/
public function add()
{
if ($this->request->isGet()) {
// 设置页面标题
$this->title = '添加回传配置';
$this->fetch('add'); // 明确指定渲染 add.html
}
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'package_id.require' => '包名ID不能为空',
'package_name.require' => '包名不能为空!',
'event_name.require' => '事件名称不能为空!',
'callback_url.require' => '回传地址不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查是否已存在相同配置
$exists = $this->app->db->name($this->table)
->where([
'package_id' => $data['package_id'],
'event_name' => $data['event_name']
])
->find();
if ($exists) {
return json(['code' => 0, 'info' => '该包名下已存在相同事件名称的配置!']);
}
// 添加创建信息
$data['create_by'] = AdminService::instance()->getUserId();
$data['create_at'] = date('Y-m-d H:i:s');
$data['update_at'] = date('Y-m-d H:i:s');
$result = $this->app->db->name($this->table)->insert($data);
if ($result === false) {
return json(['code' => 0, 'info' => '添加失败,请重试!']);
}
sysoplog('积分墙包名回传', '添加成功!');
return json(['code' => 1, 'info' => '添加成功!']);
} catch (\Exception $e) {
trace("添加回传配置异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 编辑回传配置
* @auth true
*/
public function edit()
{
if ($this->request->isGet()) {
$id = $this->request->get('id/d', 0);
if (empty($id)) {
$this->error('参数错误');
}
// 获取配置信息
$vo = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($vo)) {
$this->error('配置不存在');
}
// 检查权限
if (!$this->checkPackageAuth($vo['package_id'])) {
$this->error('您没有操作此包名的权限!');
}
$this->assign('vo', $vo);
$this->fetch(); // 直接渲染 edit.html
}
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '配置ID不能为空',
'package_id.require' => '包名ID不能为空',
'package_name.require' => '包名不能为空!',
'event_name.require' => '事件名称不能为空!',
'callback_url.require' => '回传地址不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查是否已存在相同配置(排除自身)
$exists = $this->app->db->name($this->table)
->where([
['package_id', '=', $data['package_id']],
['event_name', '=', $data['event_name']],
['id', '<>', $data['id']]
])
->find();
if ($exists) {
return json(['code' => 0, 'info' => '该包名下已存在相同事件名称的配置!']);
}
// 更新时间
$data['update_at'] = date('Y-m-d H:i:s');
$result = $this->app->db->name($this->table)
->where(['id' => $data['id']])
->update($data);
if ($result === false) {
return json(['code' => 0, 'info' => '更新失败,请重试!']);
}
sysoplog('积分墙包名回传', '更新成功!');
return json(['code' => 1, 'info' => '更新成功!']);
} catch (\Exception $e) {
trace("编辑回传配置异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 删除回传配置
* @auth true
*/
public function remove()
{
if ($this->request->isPost()) {
try {
$id = $this->request->param('id/d');
if (empty($id)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 获取配置信息
$config = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($config)) {
return json(['code' => 0, 'info' => '配置不存在!']);
}
// 检查权限
if (!$this->checkPackageAuth($config['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
$result = $this->app->db->name($this->table)->where('id', $id)->delete();
if ($result === false) {
return json(['code' => 0, 'info' => '删除失败,请重试!']);
}
sysoplog('积分墙包名回传', '删除成功!');
return json(['code' => 1, 'info' => '删除成功!']);
} catch (\Exception $e) {
trace("删除回传配置异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 检查包名权限
* @param int $packageId 包名ID
* @return bool
*/
protected function checkPackageAuth($packageId)
{
// 超级管理员直接放行
if (AdminService::instance()->isSuper()) {
return true;
}
// 检查用户是否有包名管理模块的权限
if (!AdminService::instance()->check('package/index')) {
return false;
}
// 检查具体包名权限
$userId = AdminService::instance()->getUserId();
$exists = $this->app->db->name('offer_package_auth')
->where(['package_id' => $packageId, 'user_id' => $userId])
->find();
return !empty($exists);
}
/**
* 修改状态
* @auth true
*/
public function state()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '配置ID不能为空',
'status.in:0,1' => '状态值不正确!'
]);
// 获取配置信息
$config = $this->app->db->name($this->table)->where('id', $data['id'])->find();
if (empty($config)) {
return json(['code' => 0, 'info' => '配置不存在!']);
}
// 检查权限
if (!$this->checkPackageAuth($config['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 更新状态
$result = $this->app->db->name($this->table)
->where(['id' => $data['id']])
->update([
'status' => $data['status'],
'update_at' => date('Y-m-d H:i:s')
]);
if ($result === false) {
return json(['code' => 0, 'info' => '状态更新失败!']);
}
sysoplog('积分墙包名回传', '状态更新成功!');
return json(['code' => 1, 'info' => '状态更新成功!']);
} catch (\Exception $e) {
trace("修改状态异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 搜索包名
* @auth true
*/
public function searchPackages()
{
try {
$keyword = input('keyword/s', '');
$init = input('init/d', 0); // 是否是初始化加载
$event_name = input('event_name/s', ''); // 事件名称,用于排除已配置的包名
$edit_id = input('edit_id/d', 0); // 编辑时的ID用于排除自身
// 构建查询
$query = $this->app->db->name('offer_package')
->alias('p')
->where('p.status', 1);
// 非超级管理员需要进行权限过滤
if (!AdminService::instance()->isSuper()) {
$userId = AdminService::instance()->getUserId();
$query->join('offer_package_auth a', 'p.id = a.package_id')
->where('a.user_id', $userId);
}
// 排除已经配置过此事件的包名
if (!empty($event_name)) {
$existsQuery = $this->app->db->name($this->table)
->where('event_name', $event_name);
// 编辑时排除自身
if ($edit_id > 0) {
$existsQuery->where('id', '<>', $edit_id);
}
$existsPackageIds = $existsQuery->column('package_id');
if (!empty($existsPackageIds)) {
$query->whereNotIn('p.id', $existsPackageIds);
}
}
// 如果是搜索则添加搜索条件
if (!$init && !empty($keyword)) {
$query->where(function($query) use ($keyword) {
$query->whereOr([
['p.package_name', 'like', "%{$keyword}%"],
['p.name', 'like', "%{$keyword}%"]
]);
});
}
// 限制返回数量
$list = $query->field(['p.id', 'p.package_name', 'p.name']) // 明确指定字段
->group('p.id') // 防止重复
->order('p.id desc') // 按ID倒序
->limit(20)
->select()
->toArray();
return json(['code' => 1, 'msg' => '获取成功', 'data' => $list]);
} catch (\Exception $e) {
trace("搜索包名异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 0, 'msg' => '系统异常,请稍后重试']);
}
}
/**
* 检查配置是否存在
* @auth true
*/
public function checkExists()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'package_id.require' => '包名ID不能为空',
'event_name.require' => '事件名称不能为空!'
]);
// 检查是否已存在相同配置
$exists = $this->app->db->name($this->table)
->where([
'package_id' => $data['package_id'],
'event_name' => $data['event_name']
])
->find();
return json([
'code' => 1,
'msg' => '检查成功',
'data' => !empty($exists)
]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '检查失败']);
}
}
}
/**
* 获取已存在的配置详情
* @auth true
*/
public function getExistingConfig()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'package_id.require' => '包名ID不能为空',
'event_name.require' => '事件名称不能为空!'
]);
// 获取已存在的配置
$config = $this->app->db->name($this->table)
->where([
'package_id' => $data['package_id'],
'event_name' => $data['event_name']
])
->find();
return json([
'code' => 1,
'msg' => '获取成功',
'data' => $config
]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '获取失败']);
}
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 回传记录管理
* @auth true
* @menu true
*/
class PackageCallbackRecords extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_callback_records';
/**
* 回传记录列表
* @auth true
* @menu true
*/
public function index()
{
$this->title = '回传记录列表';
$this->fetch();
}
/**
* 获取回传记录列表
* @auth true
*/
public function get_list()
{
try {
$page = input('page/d', 1);
$limit = input('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = input('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 事件名称搜索
if ($event_name = input('event_name/s', '')) {
$where[] = ['event_name', 'like', "%{$event_name}%"];
}
// 推送ID搜索
if ($push_record_id = input('push_record_id/d', '')) {
$where[] = ['push_record_id', '=', $push_record_id];
}
// 状态筛选
if ($status = input('status/s', '')) {
$where[] = ['status', '=', $status];
}
// 时间范围筛选
if ($start_time = input('start_time/s', '')) {
$where[] = ['callback_time', '>=', $start_time];
}
if ($end_time = input('end_time/s', '')) {
$where[] = ['callback_time', '<=', $end_time];
}
// 查询数据
$query = $this->app->db->name($this->table)
->alias('a')
->join('offer_package_push_records b', 'a.push_record_id = b.id', 'left')
->field('a.*, b.source_type');
// 获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->order('a.id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取回传记录列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
/**
* 查看详情
* @auth true
*/
public function detail()
{
$id = input('id/d', 0);
if (empty($id)) {
$this->error('参数错误');
}
$vo = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($vo)) {
$this->error('记录不存在');
}
$this->vo = $vo;
$this->fetch();
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 推送记录管理
* @auth true
* @menu true
*/
class PackagePushRecords extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_push_records';
/**
* 推送记录列表
* @auth true
* @menu true
*/
public function index()
{
$this->title = '推送记录列表';
$this->fetch();
}
/**
* 获取推送记录列表
* @auth true
*/
public function get_list()
{
try {
$page = input('page/d', 1);
$limit = input('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = input('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 事件名称搜索
if ($event_name = input('event_name/s', '')) {
$where[] = ['event_name', 'like', "%{$event_name}%"];
}
// 来源筛选
if ($source_type = input('source_type/s', '')) {
$where[] = ['source_type', '=', $source_type];
}
// 时间范围筛选
if ($start_time = input('start_time/s', '')) {
$where[] = ['receive_time', '>=', $start_time];
}
if ($end_time = input('end_time/s', '')) {
$where[] = ['receive_time', '<=', $end_time];
}
// 查询数据
$query = $this->app->db->name($this->table);
// 获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->field('*')
->order('id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取推送记录列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
/**
* 查看详情
* @auth true
*/
public function detail()
{
$id = input('id/d', 0);
if (empty($id)) {
$this->error('参数错误');
}
$vo = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($vo)) {
$this->error('记录不存在');
}
$this->vo = $vo;
$this->fetch();
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare (strict_types = 1);
namespace app\manager\model;
use think\admin\extend\DataExtend;
use think\Model;
/**
* 会员模型
*/
class Member extends Model
{
/**
* 数据表名称
* @var string
*/
protected $name = 'members';
/**
* 自动写入时间戳
* @var bool
*/
protected $autoWriteTimestamp = true;
/**
* 时间字段输出格式
* @var string
*/
protected $dateFormat = 'Y-m-d H:i:s';
/**
* 创建时间字段
* @var string
*/
protected $createTime = 'create_time';
/**
* 更新时间字段
* @var string
*/
protected $updateTime = 'update_time';
/**
* 追加属性
* @var array
*/
protected $append = [
'status_text'
];
/**
* 字段类型
* @var array
*/
protected $type = [
'id' => 'integer',
'status' => 'integer',
'usage_limit' => 'integer',
'used_count' => 'integer',
'points' => 'integer',
'level' => 'integer',
'balance' => 'float',
'expire_time' => 'datetime',
'last_login_time' => 'datetime',
'create_time' => 'datetime',
'update_time' => 'datetime'
];
/**
* 状态获取器
*/
public function getStatusTextAttr($value, $data)
{
return $data['status'] == 1 ? '正常' : '禁用';
}
/**
* 获取分页列表
* @param array $map 查询条件
* @param string $sort 排序规则
* @return \think\db\Query
*/
public static function mQuery($map = [], $sort = 'id desc')
{
$query = static::where($map);
// 会员搜索器
$query->withSearch(['username', 'nickname', 'mobile', 'email', 'status', 'order_id'], [
'username' => function ($query, $value) {
$query->whereLike('username|nickname', "%{$value}%");
},
'nickname' => function ($query, $value) {
$query->whereLike('nickname', "%{$value}%");
},
'mobile' => function ($query, $value) {
$query->whereLike('mobile', "%{$value}%");
},
'email' => function ($query, $value) {
$query->whereLike('email', "%{$value}%");
},
'order_id' => function ($query, $value) {
$query->whereLike('order_id', "%{$value}%");
},
'status' => function ($query, $value) {
$query->where(['status' => $value]);
}
]);
return $query->order($sort);
}
/**
* 表单处理
* @param string $template 模板名称
* @param array $data 表单数据
* @param bool $isUpdate 是否更新
* @return array|mixed
*/
public static function mForm($template = '', $data = [], $isUpdate = false)
{
if (request()->isPost()) {
$data = request()->post();
// 密码处理
if (isset($data['password']) && empty($data['password'])) {
unset($data['password']);
} elseif (isset($data['password'])) {
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
// 保存数据
if (isset($data['id']) && $data['id'] > 0) {
return static::update($data) !== false;
} else {
return static::create($data) !== false;
}
}
return $template;
}
/**
* 修改状态
* @return bool
*/
public static function mSave()
{
$data = input('post.');
return static::update($data) !== false;
}
/**
* 删除记录
* @return bool
*/
public static function mDelete()
{
$id = input('post.id');
return static::destroy($id) !== false;
}
/**
* 检查会员是否有效
* @return array
*/
public function isValid()
{
// 检查状态
if (!$this->status) {
return ['valid' => false, 'reason' => '账号已被禁用'];
}
// 检查有效期
if ($this->expire_time && $this->expire_time < date('Y-m-d H:i:s')) {
return ['valid' => false, 'reason' => '账号已过期'];
}
// 检查使用次数
if ($this->usage_limit > 0 && $this->used_count >= $this->usage_limit) {
return ['valid' => false, 'reason' => '使用次数已达上限'];
}
return ['valid' => true, 'reason' => ''];
}
/**
* 更新登录信息
* @param string $ip
* @return bool
*/
public function updateLoginInfo($ip = '')
{
$this->last_login_time = date('Y-m-d H:i:s');
$this->last_login_ip = $ip ?: request()->ip();
$this->used_count += 1;
return $this->save();
}
}

View File

@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace app\manager\sys;
use think\facade\Config;
use RedisException;
use Redis;
use RedisCluster;
class RedisManager
{
private static ?self $instance = null;
private Redis|RedisCluster|null $redis = null;
private string $prefix;
/**
* 获取RedisManager实例
*/
public static function getInstance(?array $config = null): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* 构造函数
*/
private function __construct(?array $config = null)
{
$this->initializeConnection($config);
$this->prefix = Config::get('database.connections.redis.prefix', '');
}
/**
* 初始化Redis连接
*/
private function initializeConnection(?array $config = null): void
{
$config ??= Config::get('database.connections.redis', []);
$hostName = config("app.host_name");
try {
match($hostName) {
'HHKK' => $this->initCluster([
'192.168.1.3:7000',
'192.168.1.2:7001',
'192.168.1.31:7002'
]),
'H36HK' => $this->initCluster([
'192.168.1.3:7000',
'192.168.1.2:7001',
'192.168.1.5:7002',
'192.168.1.41:7003',
'192.168.1.51:7004'
]),
default => $this->initSingle($config)
};
} catch (\Throwable $e) {
throw new RedisException("Redis连接失败: " . $e->getMessage());
}
}
/**
* 初始化单机Redis
*/
private function initSingle(array $config): void
{
$this->redis = new Redis();
$this->redis->connect(
$config['host'] ?? '127.0.0.1',
$config['port'] ?? 6379,
$config['timeout'] ?? 10
);
if (!empty($config['password'])) {
$this->redis->auth($config['password']);
}
if (isset($config['db']) && is_numeric($config['db'])) {
$this->redis->select((int)$config['db']);
}
}
/**
* 初始化Redis集群
*/
private function initCluster(array $nodes): void
{
$this->redis = new RedisCluster(null, $nodes, 5);
}
/**
* 获取Redis实例
*/
public function getRedis(): Redis|RedisCluster
{
return $this->redis;
}
/**
* String 操作
*/
public function get(string $key): mixed
{
try {
$value = $this->redis?->get($this->prefix . $key);
return $this->unserialize($value);
} catch (\Throwable) {
return null;
}
}
public function set(string $key, mixed $value, int $expire = 0): bool
{
try {
$value = $this->serialize($value);
if ($expire > 0) {
return (bool)$this->redis?->setex($this->prefix . $key, $expire, $value);
}
return (bool)$this->redis?->set($this->prefix . $key, $value);
} catch (\Throwable) {
return false;
}
}
/**
* List 操作
*/
public function lPush(string $key, mixed $value): int
{
try {
return $this->redis?->lPush($this->prefix . $key, $this->serialize($value)) ?? 0;
} catch (\Throwable) {
return 0;
}
}
public function lRange(string $key, int $start = 0, int $end = -1): array
{
try {
$data = $this->redis?->lRange($this->prefix . $key, $start, $end) ?? [];
return array_map(fn($item) => $this->unserialize($item), $data);
} catch (\Throwable) {
return [];
}
}
/**
* Hash 操作
*/
public function hSet(string $key, string $field, mixed $value): bool
{
try {
return (bool)$this->redis?->hSet(
$this->prefix . $key,
$field,
$this->serialize($value)
);
} catch (\Throwable) {
return false;
}
}
public function hGet(string $key, string|array|null $field = null): mixed
{
try {
if ($field === null) {
$data = $this->redis?->hGetAll($this->prefix . $key) ?? [];
return array_map(fn($item) => $this->unserialize($item), $data);
}
if (is_array($field)) {
$data = $this->redis?->hMGet($this->prefix . $key, $field) ?? [];
return array_map(fn($item) => $this->unserialize($item), $data);
}
$value = $this->redis?->hGet($this->prefix . $key, $field);
return $this->unserialize($value);
} catch (\Throwable) {
return null;
}
}
/**
* Set 操作
*/
public function sAdd(string $key, mixed $value): int
{
try {
if (is_array($value)) {
$count = 0;
foreach ($value as $v) {
$count += $this->redis?->sAdd(
$this->prefix . $key,
$this->serialize($v)
) ?? 0;
}
return $count;
}
return $this->redis?->sAdd($this->prefix . $key, $this->serialize($value)) ?? 0;
} catch (\Throwable) {
return 0;
}
}
public function sMembers(string $key): array
{
try {
$members = $this->redis?->sMembers($this->prefix . $key) ?? [];
return array_map(fn($item) => $this->unserialize($item), $members);
} catch (\Throwable) {
return [];
}
}
/**
* 通用操作
*/
public function delete(string|array $key): bool
{
try {
$keys = is_array($key)
? array_map(fn($k) => $this->prefix . $k, $key)
: $this->prefix . $key;
return (bool)$this->redis?->del($keys);
} catch (\Throwable) {
return false;
}
}
public function exists(string $key): bool
{
try {
return (bool)$this->redis?->exists($this->prefix . $key);
} catch (\Throwable) {
return false;
}
}
/**
* 序列化处理
*/
private function serialize(mixed $data): string
{
if (is_array($data)) {
return json_encode($data, JSON_UNESCAPED_UNICODE) ?: '';
}
return (string)$data;
}
private function unserialize(mixed $data): mixed
{
if (!is_string($data)) {
return $data;
}
$decoded = json_decode($data, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : $data;
}
private function __clone() {}
}

568
app/manager/view/error.php Normal file
View File

@@ -0,0 +1,568 @@
<?php
if (!function_exists('parse_padding')) {
function parse_padding($source)
{
$length = strlen(strval(count($source['source']) + $source['first']));
return 40 + ($length - 1) * 8;
}
}
if (!function_exists('parse_class')) {
function parse_class($name): string
{
$names = explode('\\', $name);
return '<abbr title="' . $name . '">' . end($names) . '</abbr>';
}
}
if (!function_exists('parse_file')) {
function parse_file($file, $line): string
{
return '<a class="toggle" title="' . "{$file} line {$line}" . '">' . basename($file) . " line {$line}" . '</a>';
}
}
if (!function_exists('parse_args')) {
function parse_args($args): string
{
$result = [];
foreach ($args as $key => $item) {
switch (true) {
case is_object($item):
$value = sprintf('<em>object</em>(%s)', parse_class(get_class($item)));
break;
case is_array($item):
if (count($item) > 3) {
$value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
} else {
$value = sprintf('[%s]', parse_args($item));
}
break;
case is_string($item):
if (strlen($item) > 20) {
$value = sprintf(
'\'<a class="toggle" title="%s">%s...</a>\'',
htmlentities($item),
htmlentities(substr($item, 0, 20))
);
} else {
$value = sprintf("'%s'", htmlentities($item));
}
break;
case is_int($item):
case is_float($item):
$value = $item;
break;
case is_null($item):
$value = '<em>null</em>';
break;
case is_bool($item):
$value = '<em>' . ($item ? 'true' : 'false') . '</em>';
break;
case is_resource($item):
$value = '<em>resource</em>';
break;
default:
$value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
break;
}
$result[] = is_int($key) ? $value : "'{$key}' => {$value}";
}
return implode(', ', $result);
}
}
if (!function_exists('echo_value')) {
function echo_value($val)
{
if (is_array($val) || is_object($val)) {
echo htmlentities(json_encode($val, JSON_PRETTY_PRINT));
} elseif (is_bool($val)) {
echo $val ? 'true' : 'false';
} elseif (is_scalar($val)) {
echo htmlentities($val);
} else {
echo 'Resource';
}
}
}
?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>系统发生错误</title>
<meta name="robots" content="noindex,nofollow"/>
<style>
/* Base */
body {
color: #333;
font: 16px Verdana, "Helvetica Neue", helvetica, Arial, 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0 20px 20px;
}
h1 {
margin: 10px 0 0;
font-size: 28px;
font-weight: 500;
line-height: 32px;
}
h2 {
color: #4288ce;
font-weight: 400;
padding: 6px 0;
margin: 6px 0 0;
font-size: 18px;
border-bottom: 1px solid #eee;
}
h3 {
margin: 12px;
font-size: 16px;
font-weight: bold;
}
abbr {
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
}
a {
color: #868686;
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
.line-error {
background: #f8cbcb;
}
.echo table {
width: 100%;
}
.echo pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border: 0;
border-radius: 3px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.echo pre > pre {
padding: 0;
margin: 0;
}
/* Exception Info */
.exception {
margin-top: 20px;
}
.exception .message {
padding: 12px;
border: 1px solid #ddd;
border-bottom: 0 none;
line-height: 18px;
font-size: 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .code {
float: left;
text-align: center;
color: #fff;
margin-right: 12px;
padding: 16px;
border-radius: 4px;
background: #999;
}
.exception .source-code {
padding: 6px;
border: 1px solid #ddd;
background: #f9f9f9;
overflow-x: auto;
}
.exception .source-code pre {
margin: 0;
}
.exception .source-code pre ol {
margin: 0;
color: #4288ce;
display: inline-block;
min-width: 100%;
box-sizing: border-box;
font-size: 14px;
font-family: "Century Gothic", Consolas, "Liberation Mono", Courier, Verdana, serif;
padding-left: <?php echo (isset($source) && ! empty($source)) ? parse_padding($source): 40;?> px;
}
.exception .source-code pre li {
border-left: 1px solid #ddd;
height: 18px;
line-height: 18px;
}
.exception .source-code pre code {
color: #333;
height: 100%;
display: inline-block;
border-left: 1px solid #fff;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .trace {
padding: 6px;
border: 1px solid #ddd;
border-top: 0 none;
line-height: 16px;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .trace h2:hover {
text-decoration: underline;
cursor: pointer;
}
.exception .trace ol {
margin: 12px;
}
.exception .trace ol li {
padding: 2px 4px;
}
.exception div:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* Exception Variables */
.exception-var table {
width: 100%;
margin: 12px 0;
box-sizing: border-box;
table-layout: fixed;
word-wrap: break-word;
}
.exception-var table caption {
text-align: left;
font-size: 16px;
font-weight: bold;
padding: 6px 0;
}
.exception-var table caption small {
font-weight: 300;
display: inline-block;
margin-left: 10px;
color: #ccc;
}
.exception-var table tbody {
font-size: 13px;
font-family: Consolas, "Liberation Mono", Courier, "微软雅黑", serif;
}
.exception-var table td {
padding: 0 6px;
vertical-align: top;
word-break: break-all;
}
.exception-var table td:first-child {
width: 28%;
font-weight: bold;
white-space: nowrap;
}
.exception-var table td pre {
margin: 0;
}
/* Copyright Info */
.copyright {
margin-top: 24px;
padding: 12px 0;
border-top: 1px solid #eee;
}
/* SPAN elements with the classes below are added by prettyprint. */
pre.prettyprint .pln {
color: #000
}
/* plain text */
pre.prettyprint .str {
color: #080
}
/* string content */
pre.prettyprint .kwd {
color: #008
}
/* a keyword */
pre.prettyprint .com {
color: #800
}
/* a comment */
pre.prettyprint .typ {
color: #606
}
/* a type name */
pre.prettyprint .lit {
color: #066
}
/* a literal value */
/* punctuation, lisp open bracket, lisp close bracket */
pre.prettyprint .pun, pre.prettyprint .opn, pre.prettyprint .clo {
color: #660
}
pre.prettyprint .tag {
color: #008
}
/* a markup tag name */
pre.prettyprint .atn {
color: #606
}
/* a markup attribute name */
pre.prettyprint .atv {
color: #080
}
/* a markup attribute value */
pre.prettyprint .dec, pre.prettyprint .var {
color: #606
}
/* a declaration; a variable name */
pre.prettyprint .fun {
color: red
}
/* a function name */
</style>
</head>
<body>
<?php if (\think\facade\App::isDebug()) { ?>
<?php foreach ($traces as $index => $trace) { ?>
<div class="exception">
<div class="message">
<div class="info">
<div>
<h2><?php echo "#{$index} [{$trace['code']}]" . sprintf('%s in %s', parse_class($trace['name']), parse_file($trace['file'], $trace['line'])); ?></h2>
</div>
<div><h1><?php echo nl2br(htmlentities($trace['message'])); ?></h1></div>
</div>
</div>
<?php if (!empty($trace['source'])) { ?>
<div class="source-code">
<pre class="prettyprint lang-php">
<ol start="<?php echo $trace['source']['first']; ?>"><!--<?php foreach ((array)$trace['source']['source'] as $key => $value) { ?>--><li class="line-<?php echo " {$index}-" . ($key + $trace['source']['first']) . ($trace['line'] === $key + $trace['source']['first'] ? ' line-error' : ''); ?>"><code><?php echo htmlentities($value); ?></code></li><!--<?php } ?>--></ol>
</pre>
</div>
<?php } ?>
<div class="trace">
<h2 data-expand="<?php echo 0 === $index ? '1' : '0'; ?>">Call Stack</h2>
<ol>
<li><?php echo sprintf('in %s', parse_file($trace['file'], $trace['line'])); ?></li>
<?php foreach ((array)$trace['trace'] as $value) { ?>
<li>
<?php
// Show Function
if ($value['function']) {
echo sprintf('at %s%s%s(%s)', isset($value['class']) ? parse_class($value['class']) : '', $value['type'] ?? '', $value['function'], isset($value['args']) ? parse_args($value['args']) : '');
}
// Show line
if (isset($value['file']) && isset($value['line'])) {
echo sprintf(' in %s', parse_file($value['file'], $value['line']));
}
?>
</li>
<?php } ?>
</ol>
</div>
</div>
<?php } ?>
<?php } else { ?>
<div class="exception">
<div class="info"><h1><?php echo htmlentities(isset($message) ? $message : ''); ?></h1></div>
</div>
<?php } ?>
<?php if (!empty($datas)) { ?>
<div class="exception-var">
<h2>Exception Datas</h2>
<?php foreach ((array)$datas as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array)$value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (!empty($tables)) { ?>
<div class="exception-var">
<h2>Environment Variables</h2>
<?php foreach ((array)$tables as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array)$value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (\think\facade\App::isDebug()) { ?>
<script>
function $(selector, node) {
var elements;
node = node || document;
if (document.querySelectorAll) {
elements = node.querySelectorAll(selector);
} else {
switch (selector.substr(0, 1)) {
case '#':
elements = [node.getElementById(selector.substr(1))];
break;
case '.':
if (document.getElementsByClassName) {
elements = node.getElementsByClassName(selector.substr(1));
} else {
elements = get_elements_by_class(selector.substr(1), node);
}
break;
default:
elements = node.getElementsByTagName();
}
}
return elements;
function get_elements_by_class(search_class, node, tag) {
var elements = [], eles,
pattern = new RegExp('(^|\\s)' + search_class + '(\\s|$)');
node = node || document;
tag = tag || '*';
eles = node.getElementsByTagName(tag);
for (var i = 0; i < eles.length; i++) {
if (pattern.test(eles[i].className)) {
elements.push(eles[i])
}
}
return elements;
}
}
$.getScript = function (src, func) {
var script = document.createElement('script');
script.async = 'async';
script.src = src;
script.onload = func || function () {
};
$('head')[0].appendChild(script);
}
;(function () {
var files = $('.toggle');
var ol = $('ol', $('.prettyprint')[0]);
var li = $('li', ol[0]);
// 短路径和长路径变换
for (var i = 0; i < files.length; i++) {
files[i].ondblclick = function () {
var title = this.title;
this.title = this.innerHTML;
this.innerHTML = title;
}
}
(function () {
var expand = function (dom, expand) {
var ol = $('ol', dom.parentNode)[0];
expand = undefined === expand ? dom.attributes['data-expand'].value === '0' : undefined;
if (expand) {
dom.attributes['data-expand'].value = '1';
ol.style.display = 'none';
dom.innerText = 'Call Stack (展开)';
} else {
dom.attributes['data-expand'].value = '0';
ol.style.display = 'block';
dom.innerText = 'Call Stack (折叠)';
}
};
var traces = $('.trace');
for (var i = 0; i < traces.length; i++) {
var h2 = $('h2', traces[i])[0];
expand(h2);
h2.onclick = function () {
expand(this);
};
}
})();
$.getScript('//cdn.bootcss.com/prettify/r298/prettify.min.js', function () {
prettyPrint();
});
})();
</script>
<?php } ?>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>{block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block}</title>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=0.4">
<link rel="stylesheet" href="__ROOT__/static/plugs/layui/css/layui.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/iconfont.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/console.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/extra/style.css?at={:date('md')}">
{block name="style"}{/block}
<script src="__ROOT__/static/plugs/jquery/pace.min.js"></script>
<script src="{:url('admin/api.plugs/script',[],false,false)}"></script>
</head>
<body class="layui-layout-body">
{block name='body'}
<div class="layui-layout layui-layout-admin layui-layout-left-hide">
<div class="layui-body think-bg-white margin-0 padding-0" style="top:0">{block name='content'}{/block}</div>
</div>
{/block}
<script src="__ROOT__/static/plugs/layui/layui.js"></script>
<script src="__ROOT__/static/plugs/require/require.js"></script>
<script src="__ROOT__/static/admin.js"></script>
<script src="__ROOT__/static/extra/script.js"></script>
{block name='script'}{/block}
</body>
</html>

View File

@@ -0,0 +1,16 @@
{extend name="main"}
{block name="content"}
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">欢迎页面</div>
<div class="layui-card-body">
欢迎使用管理系统{$userid}
</div>
</div>
</div>
</div>
</div>
{/block}

View File

@@ -0,0 +1,23 @@
<div class="layui-card">
{block name='style'}{/block}
{block name='header'}
{notempty name='title'}
<div class="layui-card-header">
<span class="layui-icon font-s10 color-desc margin-right-5">&#xe65b;</span>{$title|lang}
<div class="pull-right">{block name='button'}{/block}</div>
</div>
{/notempty}
{/block}
<div class="layui-card-line"></div>
<div class="layui-card-body">
<div class="layui-card-html">
{notempty name='showErrorMessage'}
<div class="think-box-notify" type="error">
<b>{:lang('系统提示:')}</b><span>{$showErrorMessage|raw}</span>
</div>
{/notempty}
{block name='content'}{/block}
</div>
</div>
{block name='script'}{/block}
</div>

View File

@@ -0,0 +1,74 @@
{extend name='../../admin/view/main'}
{block name='content'}
<form class="layui-form" action="{:sysuri()}" data-auto="true" method="post" autocomplete="off" onsubmit="return false" lay-filter="FormData">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-form-item">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-block">
<input type="text" name="email" value='{$member.email|default=""}' required lay-verify="required|email" placeholder="请输入邮箱" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">闲鱼订单号</label>
<div class="layui-input-block">
<input type="text" name="order_id" value='{$member.order_id|default=""}' required lay-verify="required" placeholder="请输入闲鱼订单号" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">有效期</label>
<div class="layui-input-block">
<input type="text" name="expire_time" value='{$member.expire_time|default=""}' required lay-verify="required" placeholder="请选择有效期" class="layui-input" id="expire_time">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">可用次数</label>
<div class="layui-input-block">
<input type="number" name="usage_limit" value='{$member.usage_limit|default="0"}' required lay-verify="required|number" placeholder="请输入可用次数" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">已用次数</label>
<div class="layui-input-block">
<input type="number" name="used_count" value='{$member.used_count|default="0"}' lay-verify="number" placeholder="请输入已用次数" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="正常" {if !isset($member.status) or $member.status eq 1}checked{/if}>
<input type="radio" name="status" value="0" title="禁用" {if isset($member.status) and $member.status eq 0}checked{/if}>
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
{notempty name='member.id'}<input type='hidden' value='{$member.id}' name='id'>{/notempty}
<button class="layui-btn" type="submit" lay-submit lay-filter="*">保存数据</button>
<button type="reset" class="layui-btn layui-btn-primary">重置数据</button>
</div>
</div>
</form>
{/block}
{block name='script'}
<script>
layui.use(['form', 'laydate'], function(){
var form = layui.form;
// 渲染日期时间选择器
layui.laydate.render({
elem: '#expire_time',
type: 'datetime'
});
});
</script>
{/block}

View File

@@ -0,0 +1,92 @@
{extend name='../../admin/view/main'}
{block name='button'}
<!-- 表格工具栏 -->
<div class="layui-btn-container">
<button class='layui-btn layui-btn-sm layui-btn-primary' data-modal='{:url("add")}' data-width="800px" data-height="600px">添加会员</button>
</div>
{/block}
{block name='content'}
<div class="think-box-shadow">
<form class="layui-form layui-form-pane form-search" action="">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">搜索</label>
<div class="layui-input-inline">
<input name="keyword" value="{$get.keyword|default=''}" placeholder="请输入邮箱/订单号" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value=''>-- 全部 --</option>
<option value='1'>正常</option>
<option value='0'>禁用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary" lay-submit lay-filter="search"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
<table id="TableList" lay-filter="TableList"></table>
</div>
{/block}
{block name='script'}
<script type="text/html" id="toolbar">
<a class="layui-btn layui-btn-sm" data-modal='{:url("edit")}?id={{d.id}}' data-width="800px" data-height="600px">编辑</a>
<a class="layui-btn layui-btn-sm layui-btn-danger" data-confirm="确定要删除此会员吗?" data-action='{:url("remove")}' data-value="id#{{d.id}}">删除</a>
</script>
<script>
layui.use(['table','form'], function(){
var table = layui.table;
var form = layui.form;
// 表格渲染
table.render({
elem: '#TableList'
,url: '{:sysuri()}?output=layui.table'
,page: true
,cellMinWidth: 200
,cols: [[
{field: 'id', title: 'ID', width: 70, align: 'center'},
{field: 'email', title: '邮箱', minWidth: 80},
{field: 'order_id', title: '闲鱼订单号', minWidth: 100},
{field: 'expire_time', title: '有效期', minWidth: 100},
{field: 'usage_limit', title: '可用次数', width: 70, align: 'center'},
{field: 'used_count', title: '已用次数', width: 70, align: 'center'},
{field: 'last_login_time', title: '最后登录时间', minWidth: 100},
{field: 'status', title: '状态', width: 100, align: 'center', templet: function(d){
return '<input type="checkbox" name="status" value="'+d.id+'" lay-skin="switch" lay-text="正常|禁用" lay-filter="statusSwitch" '+(d.status == 1 ? 'checked' : '')+'>';
}},
{field: 'create_time', title: '创建时间', minWidth: 180},
{title: '操作', toolbar: '#toolbar', width: 250, align: 'center', fixed: 'right'}
]]
});
// 搜索提交
form.on('submit(search)', function(data){
table.reload('TableList', {where: data.field});
return false;
});
// 监听状态切换
form.on('switch(statusSwitch)', function(obj){
let id = this.value;
let status = obj.elem.checked ? 1 : 0;
$.post('{:url("state")}', {id: id, status: status}, function(res){
if (res.code != 1) {
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
layer.msg(res.info, {icon: 2});
}
}, 'json');
});
});
</script>
{/block}

View File

@@ -0,0 +1,32 @@
{if isset($list)}
<fieldset>
<legend>条件搜索</legend>
<form class="layui-form layui-form-pane form-search" action="{:request()->url()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">搜索关键词</label>
<div class="layui-input-inline">
<input name="keyword" value="{$get.keyword|default=''}" placeholder="请输入用户名/昵称/手机/邮箱" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">会员状态</label>
<div class="layui-input-inline">
<select name="status">
<option value=''>-- 全部状态 --</option>
<option value='1' {if isset($get.status) and $get.status eq 1}selected{/if}>正常</option>
<option value='0' {if isset($get.status) and $get.status eq 0}selected{/if}>禁用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">注册时间</label>
<div class="layui-input-inline">
<input data-date-range name="create_time" value="{$get.create_time|default=''}" placeholder="请选择注册时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
</fieldset>
{/if}

View File

@@ -0,0 +1,294 @@
{extend name="../../admin/view/main"}
{block name='content'}
<div class="think-box-shadow">
<!-- 表单搜索 -->
<form class="layui-form layui-form-pane form-search" action="">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">应用名称</label>
<div class="layui-input-inline">
<input name="name" placeholder="请输入应用名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">所有状态</option>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">创建人</label>
<div class="layui-input-inline">
<input name="username" placeholder="请输入创建人" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">添加时间</label>
<div class="layui-input-inline">
<input name="add_time" id="add_time" placeholder="请选择添加时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary" type="button" lay-submit lay-filter="search-btn">
<i class="layui-icon">&#xe615;</i> 搜 索
</button>
<button class="layui-btn layui-btn-primary" type="reset" id="resetSearch">重置</button>
</div>
</form>
<!-- 数据表格 -->
<table id="currentTable" lay-filter="currentTable"></table>
</div>
<!-- 表单模板 -->
<script type="text/html" id="editForm">
<form class="layui-form" lay-filter="editForm" style="padding: 30px;">
<div class="layui-form-item">
<label class="layui-form-label" style="width: 100px;">应用名称</label>
<div class="layui-input-block" style="margin-left: 130px;">
<input type="text" name="name" lay-verify="required" placeholder="请输入应用名称" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="width: 100px;">包名</label>
<div class="layui-input-block" style="margin-left: 130px;">
<input type="text" name="package_name" lay-verify="required" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="width: 100px;">状态</label>
<div class="layui-input-block" style="margin-left: 130px;">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="禁用">
</div>
</div>
<div class="layui-form-item text-center" style="margin-top: 40px;">
<input type="hidden" name="id">
<button class="layui-btn layui-btn-lg" lay-submit lay-filter="formSubmit">保存</button>
<button type="button" class="layui-btn layui-btn-lg layui-btn-danger" onclick="layer.closeAll()">取消</button>
</div>
</form>
</script>
{/block}
{block name='script'}
<!-- 表格操作列 -->
<script type="text/html" id="toolbar">
<div class="layui-btn-container">
{if auth("edit")}
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
{/if}
{if auth("remove")}
<a class="layui-btn layui-btn-sm layui-btn-danger" lay-event="delete">删除</a>
{/if}
</div>
</script>
<script>
layui.use(['table', 'form', 'laydate'], function () {
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
// 日期范围
laydate.render({
elem: '#add_time',
type: 'date',
range: true,
trigger: 'click'
});
// 数据表格初始化
table.render({
elem: '#currentTable',
url: '{:request()->url()}',
method: 'post',
defaultToolbar: ['filter', 'print', 'exports'],
toolbar: ['<div class="layui-btn-container">',
'{if auth("add")}',
'<button class="layui-btn layui-btn-sm layui-btn-normal" lay-event="add"><i class="layui-icon">&#xe654;</i> 添加包名</button>',
'{/if}',
'</div>'
].join(''),
cols: [[
{type: "checkbox"},
{field: 'id', width: 80, title: 'ID', sort: true},
{field: 'name', minWidth: 150, title: '应用名称'},
{field: 'package_name', minWidth: 200, title: '包名'},
{field: 'username', width: 100, title: '创建人'},
{field: 'status', width: 100, title: '状态', templet: function(d){
return '<input type="checkbox" name="status" value="' + d.id + '" lay-skin="switch" lay-text="启用|禁用" lay-filter="status" ' + (d.status == 1 ? 'checked' : '') + '>';
}},
{field: 'add_time', minWidth: 170, title: '添加时间', templet: function(d){
return d.add_time ? layui.util.toDateString(d.add_time * 1000, 'yyyy-MM-dd HH:mm:ss') : '';
}},
{field: 'update_time', minWidth: 170, title: '更新时间', templet: function(d){
return d.update_time ? layui.util.toDateString(d.update_time * 1000, 'yyyy-MM-dd HH:mm:ss') : '';
}},
{title: '操作', minWidth: 120, toolbar: '#toolbar', align: "center", fixed: 'right'}
]],
page: true,
limit: 15,
limits: [10, 15, 20, 25, 50, 100]
});
// 搜索提交
form.on('submit(search-btn)', function (data) {
table.reload('currentTable', {
page: {curr: 1},
where: data.field,
method: 'post'
});
return false;
});
// 重置搜索
$('#resetSearch').click(function() {
// 重置表单
$('.form-search')[0].reset();
// 重载表格
table.reload('currentTable', {
page: {curr: 1},
where: {},
method: 'post'
});
});
// 监听表单提交
form.on('submit(formSubmit)', function (data) {
var url = data.field.id ? '{:url("edit")}' : '{:url("add")}';
$.ajax({
url: url,
type: 'post',
data: data.field,
success: function(res) {
if (res.code === 1) {
layer.closeAll();
layer.msg(res.info, {icon: 1, time: 1000});
table.reload('currentTable');
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
},
error: function() {
layer.msg('网络错误,请稍后重试!', {icon: 2, time: 2000});
}
});
return false;
});
// 监听工具条
table.on('tool(currentTable)', function (obj) {
var data = obj.data;
if (obj.event === 'edit') {
showForm('编辑包名', data);
} else if (obj.event === 'delete') {
layer.confirm('确定要删除该包名吗?', {
title: '删除确认',
btn: ['确定', '取消']
}, function (index) {
$.ajax({
url: '{:url("remove")}',
type: 'post',
data: {id: data.id},
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000});
table.reload('currentTable');
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
},
error: function() {
layer.msg('网络错误,请稍后重试!', {icon: 2, time: 2000});
layer.close(index);
}
});
});
}
});
// 监听头工具栏事件
table.on('toolbar(currentTable)', function(obj){
if(obj.event === 'add'){
showForm('添加包名');
}
});
// 显示表单
function showForm(title, data) {
layer.open({
type: 1,
title: title,
area: ['800px', '500px'],
offset: '50px',
shadeClose: true,
content: $('#editForm').html(),
success: function(layero, index) {
$(layero).find('.layui-form').css('margin', '15px');
$(layero).find('.layui-input-block').css('max-width', '500px');
form.render();
if(data) {
form.val('editForm', data);
}
}
});
}
// 监听状态切换
form.on('switch(status)', function(obj){
var id = this.value;
var status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status == 1 ? '启用' : '禁用') + '该包名吗?', {
title: '操作确认',
btn: ['确定', '取消']
}, function(index){
$.ajax({
url: '{:url("state")}',
type: 'post',
data: {
id: id,
status: status
},
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000});
table.reload('currentTable');
} else {
layer.msg(res.info, {icon: 2, time: 2000});
// 状态切换失败,恢复开关状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
layer.close(index);
},
error: function() {
layer.msg('网络错误请稍后重试2', {icon: 2, time: 2000});
// 发生错误,恢复开关状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
layer.close(index);
}
});
}, function(){
// 取消操作,恢复开关状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
});
});
</script>
{/block}

View File

@@ -0,0 +1,451 @@
<!-- 权限详情弹窗模板 -->
<div class="detail-container" style="padding: 20px;" data-user-id="{{d.userId}}">
<div class="layui-row layui-col-space15">
<!-- 左侧已授权包名列表 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
授权的包名列表
<div class="layui-inline" style="width: 200px; margin-left: 10px;">
<input type="text" id="searchAuthorized" placeholder="输入包名或应用名称搜索" class="layui-input">
</div>
</div>
<div class="layui-card-body auth-table-container">
<table id="authorizedTable" lay-filter="authorizedTable"></table>
</div>
</div>
</div>
<!-- 右侧可授权包名列表 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
可授权的包名列表
<span class="layui-badge layui-bg-blue">仅显示启用状态的包名</span>
<div class="layui-inline" style="width: 200px; margin-left: 10px;">
<input type="text" id="searchPackage" placeholder="输入包名或应用名称搜索" class="layui-input">
</div>
</div>
<div class="layui-card-body auth-table-container">
<table id="unauthorizedTable" lay-filter="unauthorizedTable"></table>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="layui-form-item text-center" style="margin-top:15px;">
<button class="layui-btn layui-btn-primary" onclick="closeAndRefresh()">关闭窗口</button>
</div>
</div>
<!-- 已授权表格工具栏 -->
<script type="text/html" id="authDetailAuthorizedToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="batchRemove" lay-tips="移除选中的包名权限">
<i class="layui-icon">&#xe640;</i> 批量移除
</button>
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="removeAll" lay-tips="移除该管理员的所有包名权限">
<i class="layui-icon">&#xe640;</i> 一键全部移除
</button>
</div>
</script>
<!-- 未授权表格工具栏 -->
<script type="text/html" id="authDetailUnauthorizedToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-normal" lay-event="batchAuth" lay-tips="授权选中的包名">
<i class="layui-icon">&#xe654;</i> 批量授权
</button>
<button class="layui-btn layui-btn-sm layui-btn-warm" lay-event="authAll" lay-tips="授权所有未授权的包名">
<i class="layui-icon">&#xe674;</i> 一键全部授权
</button>
</div>
</script>
<!-- 表格行工具条 -->
<script type="text/html" id="authDetailTableBar">
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="remove">移除</a>
</script>
<script type="text/html" id="authDetailUnAuthTableBar">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="auth">授权</a>
</script>
<!-- 表格初始化脚本 -->
<script>
window.initAuthDetailTables = function(userId) {
layui.use(['table', 'form'], function() {
let table = layui.table;
// 初始化已授权表格
table.render({
elem: '#authorizedTable',
url: '{:url("getAuthDetailData")}',
where: {
user_id: userId,
type: 'authorized'
},
toolbar: '#authDetailAuthorizedToolbar',
defaultToolbar: ['filter'],
page: true,
limit: 10,
limits: [10, 20, 50, 100],
cols: [[
{type: 'checkbox', width: 50},
{field: 'package_name', title: '包名', minWidth: 250},
{field: 'name', title: '名称', minWidth: 200},
{field: 'create_at', title: '授权时间', width: 160},
{title: '操作', toolbar: '#authDetailTableBar', width: 80, align: 'center', fixed: 'right'}
]]
});
// 初始化未授权表格
table.render({
elem: '#unauthorizedTable',
url: '{:url("getAuthDetailData")}',
where: {
user_id: userId,
type: 'unauthorized'
},
toolbar: '#authDetailUnauthorizedToolbar',
defaultToolbar: ['filter'],
page: true,
limit: 10,
limits: [10, 20, 50, 100],
cols: [[
{type: 'checkbox', width: 50},
{field: 'package_name', title: '包名', minWidth: 250},
{field: 'name', title: '名称', minWidth: 200},
{title: '操作', toolbar: '#authDetailUnAuthTableBar', width: 80, align: 'center', fixed: 'right'}
]]
});
// 绑定搜索事件
bindSearchEvents(table, userId);
// 绑定工具条事件
bindTableEvents(table, userId);
});
};
// 绑定搜索事件
function bindSearchEvents(table, userId) {
let searchTimeout;
$('#searchAuthorized').on('input', function() {
clearTimeout(searchTimeout);
let value = this.value;
searchTimeout = setTimeout(function() {
table.reload('authorizedTable', {
where: {
user_id: userId,
type: 'authorized',
keyword: value
},
page: {curr: 1}
});
}, 300);
});
$('#searchPackage').on('input', function() {
clearTimeout(searchTimeout);
let value = this.value;
searchTimeout = setTimeout(function() {
table.reload('unauthorizedTable', {
where: {
user_id: userId,
type: 'unauthorized',
keyword: value
},
page: {curr: 1}
});
}, 300);
});
}
// 绑定工具条事件
function bindTableEvents(table, userId) {
// 已授权表格工具条事件
table.on('toolbar(authorizedTable)', function(obj) {
if (obj.event === 'batchRemove') {
let checkStatus = table.checkStatus('authorizedTable');
if (checkStatus.data.length === 0) {
layer.msg('请选择要移除的包名', {icon: 2});
return;
}
layer.confirm('确定要移除选中的 ' + checkStatus.data.length + ' 个包名权限吗?', {
title: '批量移除确认',
btn: ['确定移除', '取消操作'],
skin: 'layer-danger'
}, function(index) {
let loadIndex = layer.load(2);
let packageIds = checkStatus.data.map(item => item.id);
$.post('{:url("batchRemoveAuth")}', {
user_id: userId,
package_ids: packageIds
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
} else if (obj.event === 'removeAll') {
layer.confirm('确定要移除该管理员的所有包名权限吗?<br>此操作不可恢复!', {
title: '危险操作',
btn: ['确定移除', '取消操作'],
icon: 2,
skin: 'layer-danger'
}, function(index) {
let loadIndex = layer.load(2);
$.post('{:url("clearAuth")}', {
user_ids: [userId]
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
}
});
// 未授权表格工具条事件
table.on('toolbar(unauthorizedTable)', function(obj) {
if (obj.event === 'batchAuth') {
let checkStatus = table.checkStatus('unauthorizedTable');
if (checkStatus.data.length === 0) {
layer.msg('请选择要授权的包名', {icon: 2});
return;
}
layer.confirm('确定要授权选中的 ' + checkStatus.data.length + ' 个包名吗?', {
title: '批量授权确认',
btn: ['确定授权', '取消操作']
}, function(index) {
let loadIndex = layer.load(2);
let packageIds = checkStatus.data.map(item => item.id);
$.post('{:url("addAuth")}', {
user_id: userId,
package_ids: packageIds
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
} else if (obj.event === 'authAll') {
layer.confirm('确定要授权所有可用的包名吗?', {
title: '一键全部授权',
btn: ['确定授权', '取消操作'],
icon: 3
}, function(index) {
let loadIndex = layer.load(2);
$.post('{:url("addAuth")}', {
user_id: userId,
is_all: 1
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
}
});
// 单个移除事件
table.on('tool(authorizedTable)', function(obj) {
if (obj.event === 'remove') {
layer.confirm('确定要移除此包名权限吗?', function(index) {
let loadIndex = layer.load(2);
$.post('{:url("removeAuth")}', {
user_id: userId,
package_id: obj.data.id
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
obj.del();
// 刷新未授权表格
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
}
});
// 单个授权事件
table.on('tool(unauthorizedTable)', function(obj) {
if (obj.event === 'auth') {
let loadIndex = layer.load(2);
$.post('{:url("addAuth")}', {
user_id: userId,
package_ids: [obj.data.id]
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
}
});
}
// 添加关闭并刷新的函数
function closeAndRefresh() {
// 关闭当前弹窗
layer.closeAll();
// 刷新父页面
parent.location.reload();
}
</script>
<!-- 弹窗专用样式 -->
<style>
.detail-container {
min-height: 600px;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
.detail-container .layui-row {
flex: 1;
}
.detail-container .layui-card {
margin-bottom: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.detail-container .layui-card-body {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
}
.auth-table-container {
min-height: 550px;
position: relative;
}
/* 搜索框样式 */
.detail-container .layui-inline {
margin: 0;
}
.detail-container .layui-input {
height: 30px;
line-height: 30px;
}
/* 底部按钮样式 */
.detail-container .layui-form-item.text-center {
padding: 10px 0 0;
margin: 0;
border-top: 1px solid #f0f0f0;
}
/* 表格美化样式 */
.auth-table-container .layui-table-tool {
background-color: #f8f8f8;
}
.auth-table-container .layui-table-page {
background-color: #f8f8f8;
}
/* 表格头部样式 */
.auth-table-container .layui-table-header {
background-color: #fafafa;
}
/* 表格内容区域样式 */
.auth-table-container .layui-table-body {
background: #fff;
}
/* 工具栏按钮样式优化 */
.auth-table-container .layui-table-tool {
padding: 10px 15px;
}
.auth-table-container .layui-table-tool .layui-btn {
height: 30px;
line-height: 30px;
padding: 0 15px;
font-size: 12px;
margin-right: 8px;
}
.auth-table-container .layui-table-tool .layui-btn:last-child {
margin-right: 0;
}
.auth-table-container .layui-table-tool .layui-btn .layui-icon {
font-size: 14px;
margin-right: 3px;
}
/* 危险操作按钮样式 */
.auth-table-container .layui-table-tool .layui-btn-danger {
background-color: #FF5722;
}
.auth-table-container .layui-table-tool .layui-btn-danger:hover {
background-color: #ff4208;
}
/* 一键全部授权按钮样式 */
.auth-table-container .layui-table-tool .layui-btn-warm {
background-color: #FFB800;
}
.auth-table-container .layui-table-tool .layui-btn-warm:hover {
background-color: #ff9900;
}
/* 弹窗样式 */
.layer-danger .layui-layer-title {
background-color: #FF5722 !important;
color: #fff !important;
}
.layer-danger .layui-layer-btn0 {
background-color: #FF5722 !important;
border-color: #FF5722 !important;
}
/* 确保弹窗控制按钮在正确位置 */
.layui-layer-page .layui-layer-setwin {
position: absolute !important;
right: 15px !important;
top: 16px !important;
}
</style>

View File

@@ -0,0 +1,786 @@
{extend name="../../admin/view/main"}
{block name='content'}
<style>
/* 基础样式 */
.layui-card-body {
padding: 15px;
}
.layui-table-tool {
background-color: #f8f8f8;
}
.layui-table-page {
background-color: #f8f8f8;
}
.layui-table-box {
background: #fff;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
}
/* 时间选择器样式 */
#dateRange {
width: 280px;
}
.layui-inline .layui-input {
height: 32px;
line-height: 32px;
}
.layui-inline .layui-btn {
height: 32px;
line-height: 32px;
}
</style>
<!-- 在主容器前添加操作指南 -->
<div class="think-box-shadow" style="margin-bottom: 15px; padding: 15px;">
<h3 class="layui-inline" style="margin-right: 15px;">操作指南</h3>
<button class="layui-btn layui-btn-sm" id="showGuide">展开/收起</button>
<div class="guide-content layui-hide" style="margin-top: 10px;">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-elem-quote" style="border-left: 5px solid #009688;">
<h4>基本说明:</h4>
<p>本页面用于管理不同管理员的包名权限,可以进行查看、添加、移除等操作。</p>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FFB800;">
<h4>操作步骤:</h4>
<ol>
<li>左侧列表显示所有可配置权限的管理员</li>
<li>绿色标签表示已授权包名数量,灰色表示未授权</li>
<li>点击【查看权限】可以查看和修改该管理员的包名权限</li>
<li>击【批量授权】可以同时给多个管理员授权相同的包名</li>
<li>点击【清空权限】可以清除选中管理员的所有包名权限</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FF5722;">
<h4>权限配置窗口说明:</h4>
<ol>
<li>左侧显示已授权的包名列表,可以搜索和移除</li>
<li>右侧显示可授权的包名列表,仅显示启用状态的包名</li>
<li>在右侧勾选包名后点击【保存配置】即可添加授权</li>
<li>两侧都支持搜索功能,可以快速查找包名</li>
<li>所有操作都会实时更新显示结果</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #01AAED;">
<h4>注意事项:</h4>
<ol>
<li>批量授权会覆盖已有的权限配置,请谨慎操作</li>
<li>清空权限操作不可恢复,请确认后再操作</li>
<li>建议先使用搜索功能查找包名,再进行授权作</li>
<li>可以使用全选功能快速选择多个管理员或包名</li>
<li>如有疑问请联系技术支持</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="think-box-shadow main-container">
<div class="layui-row layui-col-space15">
<!-- 左侧管理员列表 -->
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">
管理员列表
<div class="layui-layout-right" style="margin:5px;">
<span class="layui-badge layui-bg-blue" style="padding: 5px 10px;"> {$users|count} </span>
</div>
</div>
<div class="layui-card-body user-list-container">
<form class="layui-form" lay-filter="userForm">
<!-- 添加搜索框 -->
<div class="search-box">
<div class="layui-form-item" style="margin-bottom: 10px;">
<div class="layui-input-block" style="margin-left: 0;">
<input type="text" id="searchUsers" placeholder="输入管理员名称搜索" class="layui-input">
</div>
</div>
</div>
<!-- 全选复选框 -->
<div class="layui-form-item" style="padding: 10px;">
<input type="checkbox" lay-filter="checkAllUsers" title="全选" lay-skin="primary" lay-tips="选中/取消选中所有管理员">
</div>
<!-- 管理员列表 -->
<div class="user-list">
{foreach $users as $user}
<div class="layui-form-item">
<input type="checkbox" name="user_ids[]" value="{$user.id}" title="{$user.username}" lay-skin="primary">
{if isset($authMap[$user.id])}
<span class="layui-badge layui-bg-green" style="margin-left:10px; padding: 4px 8px;">已授权 {$authMap[$user.id]|count} 个包名</span>
<a class="layui-btn layui-btn-normal layui-btn-sm" data-user="{$user.id}" lay-event="showAuth" style="margin-left:5px;" lay-tips="点击查看和修改该管理员的包名权限配置">
<i class="layui-icon layui-icon-edit"></i> 查看权限
</a>
{else}
<span class="layui-badge layui-bg-gray" style="margin-left:10px; padding: 4px 8px;">未授权</span>
<a class="layui-btn layui-btn-warm layui-btn-sm" data-user="{$user.id}" lay-event="showAuth" style="margin-left:5px;" lay-tips="点击为该管理员添加包名权限">
<i class="layui-icon layui-icon-add-1"></i> 添加权限
</a>
{/if}
</div>
{/foreach}
</div>
</form>
</div>
</div>
</div>
<!-- 右侧包名列表 -->
<div class="layui-col-md9">
<div class="layui-card">
<div class="layui-card-header">
包名权限配置
<div class="layui-layout-right header-btns">
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="batchAuth" lay-tips="选中管理员和包名后,点击此处进行批量授权操作">
<i class="layui-icon layui-icon-add-circle"></i> 批量授权
</button>
<button class="layui-btn layui-btn-danger" id="clearAuth" lay-tips="清空选中管理员的所有包名权限,请谨慎操作">
<i class="layui-icon layui-icon-delete"></i> 清空权限
</button>
</div>
</div>
<div class="layui-card-body">
<!-- 搜索框 -->
<div class="layui-form-item" style="margin-bottom: 10px;">
<div class="layui-inline" style="width: 250px;">
<input type="text" id="searchPackages" placeholder="输入包名或应用名称搜索" class="layui-input">
</div>
<div class="layui-inline">
<input type="text" id="dateRange" placeholder="选择时间范围" class="layui-input" readonly lay-tips="选择时间范围进行筛选">
</div>
<div class="layui-inline">
<button class="layui-btn layui-btn-primary" id="resetFilter" lay-tips="清空所有筛选条件">重置</button>
</div>
</div>
<!-- 表格 -->
<table id="packageTable" lay-filter="packageTable"></table>
</div>
</div>
</div>
</div>
</div>
<!-- 修改权限详情模板引用 -->
<script type="text/html" id="authDetailTpl">
{include file="package_auth/auth_detail"}
</script>
<!-- JavaScript 部分 -->
<script>
// 全局配置
var CONFIG = {
pageSize: 10,
loadingTime: 1000,
debounceDelay: 300
};
// 消息提示
var MSG = {
networkError: '网络错误,请稍后重试!',
selectAdmin: '请选择管理员',
selectPackage: '请选择包名',
confirmClear: '确定要清空权限吗?此操作不可恢复!',
confirmBatchAuth: '确定要批量授权吗?<br>此操作将覆盖已有权限!',
getUserFailed: '获取用户信息失败',
authSuccess: '授权成功!',
clearSuccess: '清空成功!'
};
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function() {
let context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
// 成功提示
function showSuccess(msg, callback) {
layer.msg(msg, {
icon: 1,
time: CONFIG.loadingTime,
shade: 0.3
}, callback);
}
// 错误提示
function showError(msg) {
layer.msg(msg, {
icon: 2,
time: 2000,
shade: 0.3
});
}
// 表格事件处理对象
var TableEventHandler = {
// 工具条事件
tool: function(obj, callback) {
var event = obj.event;
var handlers = {
remove: function() {
layer.confirm('确定要移除此包名权限吗?', {
title: '操作确认',
btn: ['确定', '取消']
}, function(index) {
callback && callback(obj, index);
});
}
};
handlers[event] && handlers[event]();
},
// 表头工具栏事件
toolbar: function(obj, callback) {
var event = obj.event;
var handlers = {
batchRemove: function() {
var checkStatus = table.checkStatus(obj.config.id);
if (!checkStatus.data.length) {
layer.msg('请选择要移除的包名', {icon: 2});
return;
}
layer.confirm('确定要移除选中的 ' + checkStatus.data.length + ' 个包名权限吗?', {
title: '批量移除确认',
btn: ['确定', '取消']
}, function(index) {
callback && callback(checkStatus.data, index);
});
}
};
handlers[event] && handlers[event]();
}
};
// 表格缓存对象
var TableCache = {
data: {},
set: function(key, value) {
this.data[key] = value;
// 最多缓存100条数据
var keys = Object.keys(this.data);
if (keys.length > 100) {
delete this.data[keys[0]];
}
},
get: function(key) {
return this.data[key];
},
clear: function() {
this.data = {};
}
};
// 基础表格配置
var baseTableConfig = {
page: true,
limit: 15,
loading: true,
text: {
none: '暂无数据'
},
size: 'sm'
};
layui.use(['table', 'form', 'laydate'], function() {
let table = layui.table,
form = layui.form,
laydate = layui.laydate;
// 将 table 绑定到全局,以便其他函数使用
window.packageAuthTable = table;
// 初始化时间选择器
laydate.render({
elem: '#dateRange',
type: 'datetime',
range: true,
trigger: 'click',
done: function(value, date){
if(value) {
var dates = value.split(' - ');
table.reload('packageTable', {
where: {
start_time: dates[0],
end_time: dates[1]
},
page: {curr: 1}
});
}
}
});
// 重置筛选
$('#resetFilter').on('click', function(){
$('#searchPackages').val('');
$('#dateRange').val('');
table.reload('packageTable', {
where: {
keyword: '',
start_time: '',
end_time: ''
},
page: {curr: 1}
});
});
// 优化搜索功能
$('#searchPackages').on('input', debounce(function(){
var value = this.value;
var dateRange = $('#dateRange').val();
var dates = dateRange ? dateRange.split(' - ') : ['', ''];
table.reload('packageTable', {
where: {
keyword: value,
start_time: dates[0],
end_time: dates[1]
},
page: {curr: 1}
});
}, 300));
// 初始化包名权限配置表格
table.render({
elem: '#packageTable',
url: '{:url("getPackageList")}',
method: 'post',
toolbar: true,
defaultToolbar: ['filter', 'exports'],
cols: [[
{type: 'checkbox', width: 50},
{field: 'package_name', title: '包名', sort: true},
{field: 'name', title: '应用名称'}
]],
page: true,
limit: 20,
limits: [10, 20, 50, 100],
height: 'full-120',
text: {
none: '暂无包名数据'
},
size: 'sm'
});
// 管理员全选
form.on('checkbox(checkAllUsers)', function(data){
var checked = data.elem.checked;
$('input[name="user_ids[]"]').prop('checked', checked);
form.render('checkbox');
});
// 启用包名全选
form.on('checkbox(checkAllEnabled)', function(data){
var checked = data.elem.checked;
$(data.elem).closest('.layui-form-item').nextUntil('.layui-form-item:has(label:contains("禁用包名"))')
.find('input[type="checkbox"]').prop('checked', checked);
form.render('checkbox');
});
// 禁用包全选
form.on('checkbox(checkAllDisabled)', function(data){
var checked = data.elem.checked;
$(data.elem).closest('.layui-form-item').nextAll()
.find('input[type="checkbox"]').prop('checked', checked);
form.render('checkbox');
});
// 批量授权按钮事件
form.on('submit(batchAuth)', function(data) {
let userIds = [];
$('input[name="user_ids[]"]:checked').each(function() {
userIds.push($(this).val());
});
if(userIds.length === 0) {
layer.msg('请选择要授权的管理员', {icon: 2});
return false;
}
// 使用全局的 table 变量
let checkStatus = window.packageAuthTable.checkStatus('packageTable');
let packageIds = checkStatus.data.map(item => item.id);
if(packageIds.length === 0) {
layer.msg('请选择要授权的包名', {icon: 2});
return false;
}
layer.confirm('确定要批量授权吗?<br>此操作将覆盖已有权限!', {
title: '操作确认',
btn: ['确定', '取消'],
icon: 3
}, function(index) {
let loadIndex = layer.load(2);
$.ajax({
url: '{:url("batchAuth")}',
type: 'post',
data: {
user_ids: userIds,
package_ids: packageIds
},
success: function(res) {
layer.close(loadIndex);
if(res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000}, function() {
location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
}
});
});
return false;
});
// 清空权限按钮事件
$('#clearAuth').click(function() {
let userIds = [];
$('input[name="user_ids[]"]:checked').each(function() {
userIds.push($(this).val());
});
if(userIds.length === 0) {
layer.msg('请选择要清空权限的管理员', {icon: 2});
return false;
}
layer.confirm('确定要清空所选管理员的权限吗?<br>此操作不可恢复!', {
title: '危险操作',
btn: ['确定清空', '取消'],
icon: 2
}, function(index) {
let loadIndex = layer.load(2);
$.ajax({
url: '{:url("clearAuth")}',
type: 'post',
data: {
user_ids: userIds
},
success: function(res) {
layer.close(loadIndex);
if(res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000}, function() {
location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
}
});
});
});
// 操作指南展开/收起
$('#showGuide').click(function(){
var $content = $('.guide-content');
if ($content.hasClass('layui-hide')) {
$content.removeClass('layui-hide').hide().slideDown();
// 存储状态到 localStorage
localStorage.setItem('packageAuthGuideShow', '1');
} else {
$content.slideUp(function(){
$(this).addClass('layui-hide');
});
// 存储状态到 localStorage
localStorage.setItem('packageAuthGuideShow', '0');
}
});
// 页面加载时检查是否需要显示操作指南
if (localStorage.getItem('packageAuthGuideShow') === '1') {
$('.guide-content').removeClass('layui-hide');
}
// 初始化按钮提示
$('[lay-tips]').each(function(){
var tips = $(this).attr('lay-tips');
if(tips) {
layui.use('layer', function(){
var layer = layui.layer;
$(this).on('mouseenter', function(){
this.index = layer.tips(tips, this, {
tips: [1, '#3595CC'],
time: 0
});
}).on('mouseleave', function(){
layer.close(this.index);
});
}.bind(this));
}
});
});
// 辅助函数
function viewAuth(userId, username){
$('[data-user="' + userId + '"][lay-event="showAuth"]').click();
}
function clearSingleAuth(userId){
layer.confirm('确定要清空此管理员的权限吗?', {
title: '操作确认',
btn: ['确定', '取消'],
icon: 3,
skin: 'layui-layer-molv'
}, function(index){
var loadIndex = layer.load(2);
$.ajax({
url: '{:url("clearAuth")}',
type: 'post',
data: {
user_ids: [userId]
},
success: function(res){
layer.close(loadIndex);
if(res.code === 1){
layer.msg(res.info, {icon: 1, time: 1000}, function(){
location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
}
});
});
}
// 将权限详情相关的函数抽取出来
function showAuthDetail(userId, username) {
layer.open({
type: 1,
title: username + ' 的权限配置',
area: ['1200px', '80%'],
offset: '50px',
moveType: 1,
content: $('#authDetailTpl').html(),
success: function(layero) {
window.initAuthDetailTables(userId);
// 隐藏默认的关闭按钮
$(layero).find('.layui-layer-setwin').css({
'display': 'none'
});
}
});
}
// 绑定查看权限按钮事件
$(document).on('click', '[lay-event="showAuth"]', function() {
let userId = $(this).data('user');
let username = $(this).closest('.layui-form-item').find('input[type="checkbox"]').attr('title');
showAuthDetail(userId, username);
});
</script>
<!-- 添加一些样式 -->
<style>
.guide-content ol {
padding-left: 20px;
margin: 10px 0;
}
.guide-content ol li {
line-height: 28px;
color: #666;
}
.guide-content h4 {
margin: 5px 0;
color: #333;
}
.layui-elem-quote {
margin-bottom: 10px;
}
.context-menu-layer {
box-shadow: 0 2px 8px rgba(0,0,0,.1);
background: none;
}
.context-menu {
background: #fff;
border-radius: 2px;
padding: 5px 0;
}
.context-menu .menu-item {
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
color: #666;
transition: all .2s;
}
.context-menu .menu-item:hover {
background: #f2f2f2;
color: #009688;
}
.context-menu .menu-item .layui-icon {
margin-right: 5px;
font-size: 14px;
}
.context-menu .menu-item[onclick*="clearSingleAuth"] {
color: #FF5722;
}
.context-menu .menu-item[onclick*="clearSingleAuth"]:hover {
background: #fff1f0;
}
/* 管理员列表样式优化 */
.user-list .layui-form-item {
padding: 8px 0;
margin-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.user-list .layui-form-item:last-child {
border-bottom: none;
}
.user-list .layui-btn-sm {
height: 28px;
line-height: 28px;
padding: 0 12px;
}
.user-list .layui-badge {
font-size: 12px;
font-weight: normal;
}
.user-list .layui-btn-normal {
background-color: #1E9FFF;
}
.user-list .layui-btn-warm {
background-color: #FFB800;
}
.user-list .layui-icon {
font-size: 14px;
margin-right: 3px;
}
/* 美化搜索框 */
.search-box .layui-input {
height: 36px;
line-height: 36px;
border-radius: 2px;
}
/* 美化全选框 */
.layui-form-item .layui-form-checkbox[lay-skin=primary] {
margin-top: 0;
}
/* 右侧顶部按钮样式优化 */
.header-btns {
margin: 3px 0;
padding-right: 5px;
}
.header-btns .layui-btn {
height: 34px;
line-height: 34px;
padding: 0 15px;
font-size: 13px;
margin-left: 8px;
}
.header-btns .layui-btn .layui-icon {
font-size: 14px;
margin-right: 3px;
}
.header-btns .layui-btn-normal {
background-color: #1E9FFF;
}
.header-btns .layui-btn-normal:hover {
background-color: #0d8aff;
}
.header-btns .layui-btn-danger {
background-color: #FF5722;
}
.header-btns .layui-btn-danger:hover {
background-color: #ff4208;
}
/* 提示框样式优化 */
.layui-layer-tips {
font-size: 13px !important;
}
.layui-layer-tips .layui-layer-content {
padding: 8px 12px !important;
line-height: 1.5 !important;
border-radius: 2px !important;
}
.layui-layer-tips i.layui-layer-TipsT {
border-right-color: #3595CC !important;
}
/* 危险操作弹窗样式 */
.layer-danger .layui-layer-title {
background-color: #FF5722 !important;
color: #fff !important;
}
.layer-danger .layui-layer-btn0 {
background-color: #FF5722 !important;
border-color: #FF5722 !important;
}
/* 工具栏按钮样式 */
.auth-table-container .layui-table-tool .layui-btn {
margin-right: 8px;
}
.auth-table-container .layui-table-tool .layui-btn:last-child {
margin-right: 0;
}
.auth-table-container .layui-table-tool .layui-btn-danger:hover {
background-color: #ff4208;
}
/* 正常操作弹窗样式 */
.layer-normal .layui-layer-title {
background-color: #1E9FFF !important;
color: #fff !important;
}
.layer-normal .layui-layer-btn0 {
background-color: #1E9FFF !important;
border-color: #1E9FFF !important;
}
/* 工具栏按钮样式补充 */
.auth-table-container .layui-table-tool .layui-btn-normal {
background-color: #1E9FFF;
}
.auth-table-container .layui-table-tool .layui-btn-normal:hover {
background-color: #0d8aff;
}
</style>
{/block}

View File

@@ -0,0 +1,258 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="{:request()->url()}" data-auto="true" method="post">
<div class="layui-card-body">
<div class="layui-form-item">
<label class="layui-form-label">选择包名</label>
<div class="layui-input-block">
<div id="packageSelect"></div>
<input type="hidden" name="package_id" id="selectedPackageId">
<input type="hidden" name="package_name" id="selectedPackageName">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-block">
<input type="text" name="event_name" required lay-verify="required" placeholder="请输入事件名称" class="layui-input">
<tip>例如install, register, purchase </tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">回传地址</label>
<div class="layui-input-block">
<input type="text" name="callback_url" required lay-verify="required|callback_url" placeholder="请输入回传地址" class="layui-input">
<tip>
完整的回传接口地址例如http://api.example.com/callback<br>
支持变量替换,如 {package_name}, {event_name} <br>
详细说明请查看完整回传指南
</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="停用">
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
<button class="layui-btn" lay-submit lay-filter="formSubmit">保存数据</button>
<button class="layui-btn layui-btn-danger" type="button" data-close>取消添加</button>
</div>
</form>
{/block}
{block name="script"}
<style>
.layui-form tip {
display: block;
padding: 10px;
margin-top: 10px;
color: #666;
background-color: #f8f8f8;
border-radius: 2px;
line-height: 1.8;
}
.layui-badge {
margin: 2px;
font-family: Consolas, monospace;
}
</style>
<script>
$(function () {
layui.use(['form', 'xmSelect', 'layer'], function () {
let form = layui.form;
let layer = layui.layer;
let existingConfig = null; // 用于存储已存在的配置信息
// 定义包名选择器
const packageSelect = xmSelect.render({
el: '#packageSelect',
name: 'package_id',
radio: true,
clickClose: true,
filterable: true,
tips: '请选择包名',
data: [],
direction: 'auto',
model: { label: { type: 'text' } },
on: function(data) {
if(data.arr.length > 0) {
let selected = data.arr[0];
$('#selectedPackageId').val(selected.value);
$('#selectedPackageName').val(selected.package_name);
// 选择包名后,如果已有事件名称,则检查是否存在
checkEventExists();
} else {
$('#selectedPackageId').val('');
$('#selectedPackageName').val('');
}
form.render();
}
});
let checkTimer = null; // 添加防抖定时器
// 监听事件名称输入框
$('input[name="event_name"]').on('input propertychange', function() {
let currentValue = $(this).val();
console.log('事件名称输入:', currentValue);
// 使用防抖,避免频繁请求
if (checkTimer) clearTimeout(checkTimer);
checkTimer = setTimeout(function() {
checkEventExists(currentValue);
}, 300);
});
// 检查事件是否存在
function checkEventExists(eventValue) {
// 获取当前弹窗内的元素
let $dialog = $('.layui-layer-page').last();
let packageId = $('#selectedPackageId').val();
let $eventInput = $dialog.find('input[name="event_name"]');
// 清除现有提示和验证状态
$eventInput.removeClass('layui-form-danger');
$eventInput.nextAll('.layui-form-mid,.layui-word-aux').remove();
$dialog.find('tip').show();
// 重置existingConfig
existingConfig = null;
if (packageId && eventValue) {
// 直接获取配置详情
$.ajax({
url: '{:url("getExistingConfig")}',
type: 'POST',
data: {
package_id: packageId,
event_name: eventValue
},
dataType: 'json',
success: function(configRes) {
if (configRes.code === 1 && configRes.data) {
// 保存已存在的配置信息
existingConfig = configRes.data; // 保存完整的配置信息
// 添加验证失败样式
$eventInput.addClass('layui-form-danger');
// 隐藏当前弹窗内的tip
$dialog.find('.layui-input-block tip').hide();
// 添加提示信息
$eventInput.after(`
<div class="layui-form-mid" style="margin-top: 5px;">
<div style="color: #FF5722; font-weight: bold; margin-bottom: 5px;">
<i class="layui-icon layui-icon-warning" style="font-size: 16px; margin-right: 5px;"></i>
该包名下已存在此事件名称的配置,提交时将覆盖现有配置
</div>
<div style="color: #666; background: #f8f8f8; padding: 8px; border-radius: 2px; margin-top: 5px;">
<i class="layui-icon layui-icon-link" style="margin-right: 5px;"></i>
已配置的回调地址:${configRes.data.callback_url}
</div>
</div>
`);
}
}
});
}
}
// 加载包名列表
function loadPackageList() {
$.get('{:url("searchPackages")}', {
init: 1
}, function(res) {
if (res.code === 1) {
let data = res.data.map(function(item) {
return {
name: item.package_name + ' (' + item.name + ')',
value: item.id,
package_name: item.package_name
};
});
packageSelect.update({
data: data,
autoRow: true
});
}
}, 'json');
}
// 初始加载包名列表
loadPackageList();
// 表单验证
form.verify({
callback_url: function(value) {
if (!/^https?:\/\/.+/.test(value)) {
return '请输入正确的URL地址';
}
}
});
// 修改表单提交处理
form.on('submit(formSubmit)', function(data) {
// 如果存在配置,显示确认框
if (existingConfig) {
layer.confirm('该包名下已存在此事件配置,是否确认覆盖?', {
title: '覆盖确认',
btn: ['确认覆盖', '取消'],
icon: 3
}, function(index) {
// 确认覆盖,将请求转为更新操作
let formData = data.field;
formData.id = existingConfig.id; // 添加已存在配置的ID
$.ajax({
url: '{:url("edit")}',
type: 'POST',
data: formData,
dataType: 'json',
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1500}, function() {
parent.layer.close(parent.layer.getFrameIndex(window.name));
parent.location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 1500});
}
}
});
layer.close(index);
});
} else {
// 如果不存在配置,使用默认表单提交
let formData = data.field;
$.ajax({
url: '{:url("add")}',
type: 'POST',
data: formData,
dataType: 'json',
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1500}, function() {
parent.layer.close(parent.layer.getFrameIndex(window.name));
parent.location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 1500});
}
}
});
}
return false; // 阻止表单默认提交
});
});
});
</script>
{/block}

View File

@@ -0,0 +1,160 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="{:request()->url()}" data-auto="true" method="post">
<div class="layui-card-body">
<input type="hidden" name="id" value="{$vo.id|default=''}">
<div class="layui-form-item">
<label class="layui-form-label">选择包名</label>
<div class="layui-input-block">
<div id="packageSelect"></div>
<input type="hidden" name="package_id" id="selectedPackageId" value="{$vo.package_id|default=''}">
<input type="hidden" name="package_name" id="selectedPackageName" value="{$vo.package_name|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-block">
<input type="text" name="event_name" required lay-verify="required" placeholder="请输入事件名称" class="layui-input" value="{$vo.event_name|default=''}">
<tip>例如install, register, purchase </tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">回传地址</label>
<div class="layui-input-block">
<input type="text" name="callback_url" required lay-verify="required|callback_url" placeholder="请输入回传地址" class="layui-input" value="{$vo.callback_url|default=''}">
<tip>
完整的回传接口地址例如http://api.example.com/callback<br>
支持变量替换,如 {package_name}, {event_name} <br>
详细说明请查看完整回传指南
</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" {if $vo.status eq 1}checked{/if}>
<input type="radio" name="status" value="0" title="停用" {if $vo.status eq 0}checked{/if}>
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
<button class="layui-btn" lay-submit lay-filter="formSubmit">保存数据</button>
<button class="layui-btn layui-btn-danger" type="button" data-close>取消编辑</button>
</div>
</form>
{/block}
{block name="script"}
<style>
.layui-form tip {
display: block;
padding: 10px;
margin-top: 10px;
color: #666;
background-color: #f8f8f8;
border-radius: 2px;
line-height: 1.8;
}
.layui-badge {
margin: 2px;
font-family: Consolas, monospace;
}
</style>
<script>
$(function () {
layui.use(['form', 'xmSelect'], function () {
let form = layui.form;
// 定义包名选择器
const packageSelect = xmSelect.render({
el: '#packageSelect',
name: 'package_id',
radio: true,
clickClose: true,
filterable: true,
tips: '请选择包名',
data: [],
direction: 'auto',
model: { label: { type: 'text' } },
initValue: [{$vo.package_id|default=0}], // 设置初始选中值
on: function(data) {
if(data.arr.length > 0) {
let selected = data.arr[0];
$('#selectedPackageId').val(selected.value);
$('#selectedPackageName').val(selected.package_name);
} else {
$('#selectedPackageId').val('');
$('#selectedPackageName').val('');
}
form.render();
}
});
// 加载包名列表并选中当前包名
function loadPackageList() {
$.get('{:url("searchPackages")}', {
init: 1,
edit_id: '{$vo.id|default=0}' // 传递当前编辑的ID
}, function(res) {
if (res.code === 1) {
let data = res.data.map(function(item) {
return {
name: item.package_name + ' (' + item.name + ')',
value: item.id,
package_name: item.package_name,
selected: item.id == '{$vo.package_id|default=0}' // 标记当前选中项
};
});
packageSelect.update({
data: data,
autoRow: true
});
}
}, 'json');
}
// 初始加载包名列表
loadPackageList();
// 表单验证
form.verify({
callback_url: function(value) {
if (!/^https?:\/\/.+/.test(value)) {
return '请输入正确的URL地址';
}
}
});
// 表单提交
form.on('submit(formSubmit)', function(data) {
let formData = data.field;
$.ajax({
url: '{:url("edit")}',
type: 'POST',
data: formData,
dataType: 'json',
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1500}, function() {
parent.layer.close(parent.layer.getFrameIndex(window.name));
parent.location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 1500});
}
}
});
return false;
});
});
});
</script>
{/block}

View File

@@ -0,0 +1,232 @@
{extend name="../../admin/view/main"}
{block name="content"}
<!-- 操作指南开始 -->
<div class="think-box-shadow" style="margin-bottom: 15px; padding: 15px;">
<h3 class="layui-inline" style="margin-right: 15px;">回传配置指南</h3>
<button class="layui-btn layui-btn-sm" id="showGuide">展开/收起</button>
<div class="guide-content layui-hide" style="margin-top: 10px;">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-elem-quote" style="border-left: 5px solid #009688;">
<h4>基本说明:</h4>
<p>本页面用于管理事件回传配置,支持变量替换功能,可以灵活配置回传地址。</p>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FFB800;">
<h4>支持的变量:</h4>
<ol>
<li><code>{package_name}</code> - 应用包名com.example.app</li>
<li><code>{event_name}</code> - 事件名称install, register</li>
<li><code>{event_time}</code> - 事件发生时间格式YYYY-MM-DD HH:mm:ss</li>
<li><code>{gaid}</code> - 广告ID</li>
<li><code>{network_name}</code> - 网络名称AppsFlyer, Kochava</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FF5722;">
<h4>配置示例:</h4>
<ol>
<li>基础URLhttp://api.example.com/callback</li>
<li>
完整示例http://api.example.com/callback?package={package_name}&event={event_name}&time={event_time}&gaid={gaid}&network={network_name}
</li>
<li>实际回传时,花括号中的变量会被替换为实际值</li>
<li>所有特殊字符会自动进行URL编码</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #01AAED;">
<h4>注意事项:</h4>
<ol>
<li>变量名称必须使用大括号{}包裹,且区分大小写</li>
<li>同一个包名下不能配置重复的事件名称</li>
<li>回传地址必须以http://或https://开头</li>
<li>建议在测试环境验证回传地址的正确性</li>
<li>可以通过状态开关临时停用某个回传配置</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<!-- 操作指南结束 -->
<!-- 表单搜索区域 -->
<div class="think-box-shadow">
<div class="layui-row">
<div class="layui-col-md11">
<form class="layui-form layui-form-pane form-search" action="">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-inline">
<input name="event_name" placeholder="请输入事件名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status" lay-search>
<option value="">所有状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary" lay-submit lay-filter="search_form">
<i class="layui-icon">&#xe615;</i> 搜 索
</button>
</div>
</form>
</div>
<div class="layui-col-md1 text-right">
<!--{if auth("add")}-->
<button class='layui-btn layui-btn-normal' data-modal='{:url("add")}'
data-title="添加回传配置">
<i class="layui-icon layui-icon-add-circle"></i> 添加事件
</button>
<!--{/if}-->
</div>
</div>
</div>
<!-- 数据表格区域 -->
<div class="think-box-shadow">
<table class="layui-table" id="CallbackTable" data-url="{:url('get_list')}"
data-target-search="form.form-search"></table>
</div>
<!-- 数据操作工具条模板 -->
<script type="text/html" id="toolbar">
<!--{if auth("edit")}-->
<a class="layui-btn layui-btn-sm" data-modal="{:url('edit')}?id={{d.id}}" data-title="编辑回传配置">
<i class="layui-icon layui-icon-edit"></i> 编辑
</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a class="layui-btn layui-btn-sm layui-btn-danger" data-action="{:url('remove')}" data-value="id#{{d.id}}"
data-confirm="确定要删除此配置吗?">
<i class="layui-icon layui-icon-delete"></i> 删除
</a>
<!--{/if}-->
</script>
<!-- 数据状态切换模板 -->
<script type="text/html" id="statusTpl">
<!--{if auth("state")}-->
<input type="checkbox" value="{{d.id}}" lay-skin="switch" lay-text="启用|停用" lay-filter="status"
{{d.status>0?'checked':''}}>
<!--{else}-->
{{d.status ? '<b class="color-green">启用</b>' : '<b class="color-red">停用</b>'}}
<!--{/if}-->
</script>
{/block}
{block name="script"}
<script>
$(function () {
let table; // 定义表格变量
// 初始化表格组件
table = $('#CallbackTable').layTable({
url: '{:url("get_list")}',
method: 'get',
even: true,
page: true,
limit: 15,
limits: [10, 15, 20, 25, 50, 100],
cols: [[
{field: 'id', title: 'ID', width: 80, align: 'center'},
{field: 'package_name', title: '包名', minWidth: 200},
{field: 'event_name', title: '事件名称', minWidth: 150},
{field: 'callback_url', title: '回传地址', minWidth: 300},
{field: 'status', title: '状态', width: 100, align: 'center', templet: '#statusTpl'},
{field: 'create_at', title: '创建时间', width: 180, align: 'center'},
{title: '操作', toolbar: '#toolbar', width: 180, align: 'center', fixed: 'right'}
]],
text: {
none: '暂无回传配置数据'
}
});
// 数据状态切换操作
form.on('switch(status)', function (obj) {
$.ajax({
url: '{:url("state")}',
type: 'POST',
data: {
id: obj.value,
status: obj.elem.checked ? 1 : 0
},
dataType: 'json',
success: function (res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1});
// 仅重载表格数据
table.reload('CallbackTable', {
page: {
curr: $(".layui-laypage-em").next().html() // 保持在当前页
}
});
} else {
layer.msg(res.info, {icon: 2});
// 状态切换失败回滚switch状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
},
error: function () {
layer.msg('操作失败,请重试', {icon: 2});
// 发生错误时也回滚switch状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
});
});
// 操作指南展开/收起
$('#showGuide').on('click', function () {
$('.guide-content').toggleClass('layui-hide');
});
});
</script>
<style>
/* 操作指南样式 */
.guide-content .layui-elem-quote {
margin: 10px 0;
padding: 15px;
background-color: #f8f8f8;
}
.guide-content h4 {
margin-bottom: 10px;
font-weight: bold;
}
.guide-content code {
padding: 2px 4px;
background-color: #f1f1f1;
border-radius: 3px;
color: #333;
font-family: Consolas, monospace;
}
.guide-content ol li {
margin: 5px 0;
color: #666;
}
</style>
{/block}

View File

@@ -0,0 +1,58 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="javascript:void(0)" autocomplete="off">
<div class="layui-card-body padding-left-40">
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">包名</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.package_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">事件名称</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.event_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">回传URL模板</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.callback_url|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">实际回传URL</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.final_url|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">响应状态码</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.response_code|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">响应内容</label>
<div class="layui-col-xs10">
<pre class="layui-code">{$vo.response_body|default=''}</pre>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">状态</label>
<div class="layui-col-xs10">
<div class="layui-input-line">
{if $vo.status==1}<span class="layui-badge layui-bg-green">成功</span>{else}<span class="layui-badge layui-bg-red">失败</span>{/if}
</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">回传时间</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.callback_time|default=''}</div>
</div>
</div>
</div>
</form>
{/block}

View File

@@ -0,0 +1,87 @@
{extend name="../../admin/view/main"}
{block name="button"}
{/block}
{block name="content"}
<div class="think-box-shadow">
<fieldset>
<legend>条件搜索</legend>
<form class="layui-form layui-form-pane form-search" action="javascript:void(0)" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" value="{$get.package_name|default=''}" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-inline">
<input name="event_name" value="{$get.event_name|default=''}" placeholder="请输入事件名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">推送ID</label>
<div class="layui-input-inline">
<input name="push_record_id" value="{$get.push_record_id|default=''}" placeholder="请输入推送ID" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status" class="layui-select">
<option value="">所有状态</option>
<option value="1">成功</option>
<option value="0">失败</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">回传时间</label>
<div class="layui-input-inline">
<input data-date-range name="callback_time" value="{$get.callback_time|default=''}" placeholder="请选择回传时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
</fieldset>
<table id="RecordTable" data-url="{:sysuri('get_list')}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name="script"}
<script>
$(function () {
$('#RecordTable').layTable({
even: true, height: 'full',
sort: {field: 'id', type: 'desc'},
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'push_record_id', title: '推送ID', width: 150, templet: function(d){
var source = d.source_type || '-';
return '<a class="layui-link" data-title="查看推送详情" data-open="{:url("package_push_records/detail")}?id='+ d.push_record_id +'">'+ d.push_record_id +'('+ source +')</a>';
}},
{field: 'package_name', title: '包名', minWidth: 100},
{field: 'event_name', title: '事件名称', minWidth: 100},
{field: 'callback_url', title: '回传URL模板', minWidth: 200, templet: function(d){
return '<div class="layui-elip" title="'+ d.callback_url +'">'+ d.callback_url +'</div>';
}},
{field: 'final_url', title: '实际回传URL', minWidth: 200, templet: function(d){
return '<div class="layui-elip" title="'+ d.final_url +'">'+ d.final_url +'</div>';
}},
{field: 'response_code', title: '响应码', width: 100},
{field: 'status', title: '状态', width: 100, templet: function(d){
return d.status == 1 ? '<span class="layui-badge layui-bg-green">成功</span>' : '<span class="layui-badge layui-bg-red">失败</span>';
}},
{field: 'callback_time', title: '回传时间', width: 180, sort: true},
{title: '操作', toolbar: '#toolbar', width: 100, align: 'center', fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<a class="layui-btn layui-btn-xs" data-title="查看详情" data-open="{:url('detail')}?id={{d.id}}">详情</a>
</script>
{/block}

View File

@@ -0,0 +1,44 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="javascript:void(0)" autocomplete="off">
<div class="layui-card-body padding-left-40">
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">来源</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.source_type|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">包名</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.package_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">事件名称</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.event_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">原始请求URL</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.original_url|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">接收时间</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.receive_time|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">创建时间</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.created_at|default=''}</div>
</div>
</div>
</div>
</form>
{/block}

View File

@@ -0,0 +1,72 @@
{extend name="../../admin/view/main"}
{block name="button"}
{/block}
{block name="content"}
<div class="think-box-shadow">
<fieldset>
<legend>条件搜索</legend>
<form class="layui-form layui-form-pane form-search" action="javascript:void(0)" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" value="{$get.package_name|default=''}" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-inline">
<input name="event_name" value="{$get.event_name|default=''}" placeholder="请输入事件名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">来源</label>
<div class="layui-input-inline">
<select name="source_type" class="layui-select">
<option value="">所有来源</option>
<option value="AppsFlyer">AppsFlyer</option>
<option value="Kochava">Kochava</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">接收时间</label>
<div class="layui-input-inline">
<input data-date-range name="receive_time" value="{$get.receive_time|default=''}" placeholder="请选择接收时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
</fieldset>
<table id="RecordTable" data-url="{:sysuri('get_list')}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name="script"}
<script>
$(function () {
$('#RecordTable').layTable({
even: true, height: 'full',
sort: {field: 'id', type: 'desc'},
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'source_type', title: '来源', width: 100},
{field: 'package_name', title: '包名', minWidth: 100},
{field: 'event_name', title: '事件名称', minWidth: 100},
{field: 'original_url', title: '原始请求URL', minWidth: 200, templet: function(d){
return '<div class="layui-elip" title="'+ d.original_url +'">'+ d.original_url +'</div>';
}},
{field: 'receive_time', title: '接收时间', width: 180, sort: true},
{field: 'created_at', title: '创建时间', width: 180},
{title: '操作', toolbar: '#toolbar', width: 100, align: 'center', fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<a class="layui-btn layui-btn-xs" data-title="查看详情" data-open="{:url('detail')}?id={{d.id}}">详情</a>
</script>
{/block}

View File

@@ -0,0 +1,23 @@
<div class="layui-card">
{block name='style'}{/block}
{block name='header'}
{notempty name='title'}
<div class="layui-card-header">
<span class="layui-icon font-s10 color-desc margin-right-5">&#xe65b;</span>{$title|lang}
<div class="pull-right">{block name='button'}{/block}</div>
</div>
{/notempty}
{/block}
<div class="layui-card-line"></div>
<div class="layui-card-body">
<div class="layui-card-table">
{notempty name='showErrorMessage'}
<div class="think-box-notify" type="error">
<b>{:lang('系统提示:')}</b><span>{$showErrorMessage|raw}</span>
</div>
{/notempty}
{block name='content'}{/block}
</div>
</div>
{block name='script'}{/block}
</div>