chore: 迁移依赖、移除sqlite3_flutter_libs并新增功能
1. 替换hive_flutter为hive_ce_flutter依赖 2. 从各平台插件列表移除sqlite3_flutter_libs 3. 重构API请求体格式,优化历史记录去重逻辑 4. 新增CTC笔记相关功能:桌面小部件、模板模型、本地存储 5. 新增表单收集服务和后台管理接口 6. 优化缓存配置、多语言文案和UI细节 7. 重构首页状态监听组件
This commit is contained in:
185
docs/toolsapi/application/admin/controller/FormCollect.php
Normal file
185
docs/toolsapi/application/admin/controller/FormCollect.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
|
||||
/**
|
||||
* 表单收集管理
|
||||
* @icon fa fa-wpforms
|
||||
* @time 2026-06-15
|
||||
* @description 管理表单收集记录,查看/导出/标记处理状态
|
||||
*/
|
||||
class FormCollect extends Backend
|
||||
{
|
||||
protected $model = null;
|
||||
protected $searchFields = 'id,email,source,title';
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
$this->model = new \app\admin\model\FormCollect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 安装/初始化数据表
|
||||
* @desc 创建tool_form_collect表
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$sql = <<<SQL
|
||||
CREATE TABLE IF NOT EXISTS `tool_form_collect` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(256) NOT NULL DEFAULT '' COMMENT '邮箱地址',
|
||||
`source` varchar(64) NOT NULL DEFAULT '' COMMENT '来源标识',
|
||||
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '表单标题(自动生成)',
|
||||
`extra_json` text COMMENT '扩展字段JSON',
|
||||
`uid` varchar(128) NOT NULL DEFAULT '' COMMENT '用户ID',
|
||||
`device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '设备ID',
|
||||
`ip` varchar(64) NOT NULL DEFAULT '' COMMENT '提交IP',
|
||||
`status` enum('pending','processed','invalid') NOT NULL DEFAULT 'pending' COMMENT '状态',
|
||||
`admin_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '管理员备注',
|
||||
`created_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_email` (`email`),
|
||||
KEY `idx_source` (`source`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表单收集记录';
|
||||
SQL;
|
||||
|
||||
try {
|
||||
\think\Db::execute($sql);
|
||||
$this->success('安装成功');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('安装失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 导出CSV
|
||||
* @desc 导出表单收集记录为CSV文件
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
$ids = $this->request->param('ids', '');
|
||||
$source = $this->request->param('source', '');
|
||||
$status = $this->request->param('status', '');
|
||||
|
||||
$query = \think\Db::name('form_collect');
|
||||
|
||||
if (!empty($ids)) {
|
||||
$query->where('id', 'in', $ids);
|
||||
}
|
||||
if (!empty($source)) {
|
||||
$query->where('source', $source);
|
||||
}
|
||||
if (!empty($status)) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$list = $query->order('id', 'desc')->select();
|
||||
|
||||
$filename = 'form_collect_' . date('Ymd_His') . '.csv';
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename=' . $filename);
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
// BOM for Excel UTF-8
|
||||
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// 表头
|
||||
fputcsv($output, ['ID', '邮箱', '来源', '标题', '用户ID', '设备ID', 'IP', '状态', '管理员备注', '提交时间']);
|
||||
|
||||
foreach ($list as $row) {
|
||||
fputcsv($output, [
|
||||
$row['id'],
|
||||
$row['email'],
|
||||
$row['source'],
|
||||
$row['title'],
|
||||
$row['uid'],
|
||||
$row['device_id'],
|
||||
$row['ip'],
|
||||
$row['status'],
|
||||
$row['admin_remark'],
|
||||
date('Y-m-d H:i:s', $row['created_at']),
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 标记单条已处理
|
||||
* @desc 将指定记录标记为已处理
|
||||
*/
|
||||
public function mark_processed()
|
||||
{
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->error('请选择记录');
|
||||
}
|
||||
|
||||
$idArr = is_array($ids) ? $ids : explode(',', $ids);
|
||||
$updated = \think\Db::name('form_collect')
|
||||
->where('id', 'in', $idArr)
|
||||
->update(['status' => 'processed', 'updated_at' => time()]);
|
||||
|
||||
$this->success("已处理 {$updated} 条记录");
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 批量标记已处理
|
||||
* @desc 将选中记录标记为已处理
|
||||
*/
|
||||
public function batch_process()
|
||||
{
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->error('请选择记录');
|
||||
}
|
||||
|
||||
$idArr = explode(',', $ids);
|
||||
$updated = \think\Db::name('form_collect')
|
||||
->where('id', 'in', $idArr)
|
||||
->update(['status' => 'processed', 'updated_at' => time()]);
|
||||
|
||||
$this->success("已处理 {$updated} 条记录");
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 统计数据
|
||||
* @desc 返回各来源的提交统计
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
$total = \think\Db::name('form_collect')->count();
|
||||
$pending = \think\Db::name('form_collect')->where('status', 'pending')->count();
|
||||
$processed = \think\Db::name('form_collect')->where('status', 'processed')->count();
|
||||
$invalid = \think\Db::name('form_collect')->where('status', 'invalid')->count();
|
||||
|
||||
// 按来源统计
|
||||
$bySource = \think\Db::name('form_collect')
|
||||
->field('source, COUNT(*) as cnt')
|
||||
->group('source')
|
||||
->order('cnt', 'desc')
|
||||
->select();
|
||||
|
||||
// 今日新增
|
||||
$todayStart = strtotime(date('Y-m-d'));
|
||||
$todayCount = \think\Db::name('form_collect')
|
||||
->where('created_at', '>=', $todayStart)
|
||||
->count();
|
||||
|
||||
$this->success('ok', [
|
||||
'total' => $total,
|
||||
'pending' => $pending,
|
||||
'processed' => $processed,
|
||||
'invalid' => $invalid,
|
||||
'today' => $todayCount,
|
||||
'by_source' => $bySource,
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
docs/toolsapi/application/admin/lang/zh-cn/form_collect.php
Normal file
22
docs/toolsapi/application/admin/lang/zh-cn/form_collect.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'Id' => 'ID',
|
||||
'Email' => '邮箱地址',
|
||||
'Source' => '来源',
|
||||
'Title' => '标题',
|
||||
'Extra_json' => '扩展信息',
|
||||
'Uid' => '用户ID',
|
||||
'Device_id' => '设备ID',
|
||||
'Ip' => 'IP地址',
|
||||
'Status' => '状态',
|
||||
'Admin_remark' => '管理员备注',
|
||||
'Created_at' => '提交时间',
|
||||
'Updated_at' => '更新时间',
|
||||
'Source app_gp_beta' => 'Google Play内测',
|
||||
'Source register_subscribe' => '闲言邮箱订阅',
|
||||
'Source beta_questionnaire' => 'Beta问卷',
|
||||
'Status pending' => '待处理',
|
||||
'Status processed' => '已处理',
|
||||
'Status invalid' => '无效',
|
||||
];
|
||||
45
docs/toolsapi/application/admin/model/FormCollect.php
Normal file
45
docs/toolsapi/application/admin/model/FormCollect.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 表单收集模型
|
||||
* @time 2026-06-15
|
||||
* @description 管理表单收集记录数据
|
||||
*/
|
||||
class FormCollect extends Model
|
||||
{
|
||||
protected $name = 'form_collect';
|
||||
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
protected $createTime = 'created_at';
|
||||
protected $updateTime = 'updated_at';
|
||||
protected $deleteTime = false;
|
||||
|
||||
/**
|
||||
* 状态列表
|
||||
*/
|
||||
public function getStatusList()
|
||||
{
|
||||
return [
|
||||
'pending' => '待处理',
|
||||
'processed' => '已处理',
|
||||
'invalid' => '无效',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源列表
|
||||
*/
|
||||
public function getSourceList()
|
||||
{
|
||||
return [
|
||||
'app_gp_beta' => 'Google Play内测',
|
||||
'register_subscribe' => '闲言邮箱订阅',
|
||||
'beta_questionnaire' => 'Beta问卷',
|
||||
];
|
||||
}
|
||||
}
|
||||
58
docs/toolsapi/application/admin/view/form_collect/add.html
Normal file
58
docs/toolsapi/application/admin/view/form_collect/add.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-email" data-rule="required" class="form-control" name="row[email]" type="email" placeholder="邮箱地址">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Source')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<select id="c-source" class="form-control" name="row[source]">
|
||||
<option value="app_gp_beta">Google Play内测</option>
|
||||
<option value="register_subscribe">闲言邮箱订阅</option>
|
||||
<option value="beta_questionnaire">Beta问卷</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-title" class="form-control" name="row[title]" type="text" placeholder="留空则根据来源自动生成">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Extra_json')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<textarea id="c-extra_json" class="form-control" name="row[extra_json]" rows="3" placeholder='扩展字段JSON,如: {"device":"iPhone"}'></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Uid')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-uid" class="form-control" name="row[uid]" type="text" placeholder="用户ID">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<select id="c-status" class="form-control" name="row[status]">
|
||||
<option value="pending">待处理</option>
|
||||
<option value="processed">已处理</option>
|
||||
<option value="invalid">无效</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Admin_remark')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-admin_remark" class="form-control" name="row[admin_remark]" type="text" placeholder="管理员备注">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
56
docs/toolsapi/application/admin/view/form_collect/edit.html
Normal file
56
docs/toolsapi/application/admin/view/form_collect/edit.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-email" data-rule="required" class="form-control" name="row[email]" type="email" value="{$row.email|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Source')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input class="form-control" type="text" value="{$row.source}" disabled style="background:#f5f5f5">
|
||||
<input type="hidden" name="row[source]" value="{$row.source}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input class="form-control" type="text" value="{$row.title|htmlentities}" disabled style="background:#f5f5f5">
|
||||
<input type="hidden" name="row[title]" value="{$row.title|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Uid')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input class="form-control" type="text" value="{$row.uid|htmlentities}" disabled style="background:#f5f5f5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Ip')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input class="form-control" type="text" value="{$row.ip|htmlentities}" disabled style="background:#f5f5f5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="border-top:1px dashed #ddd;padding-top:15px;margin-top:10px">
|
||||
<label class="control-label col-xs-12 col-sm-2" style="font-weight:bold;color:#333">处理状态:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<select id="c-status" class="form-control" name="row[status]" style="font-size:14px">
|
||||
<option value="pending" {if $row.status=='pending'}selected{/if}>⏳ 待处理</option>
|
||||
<option value="processed" {if $row.status=='processed'}selected{/if}>✅ 已处理</option>
|
||||
<option value="invalid" {if $row.status=='invalid'}selected{/if}>❌ 无效</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2" style="font-weight:bold;color:#333">管理员备注:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<textarea id="c-admin_remark" class="form-control" name="row[admin_remark]" rows="3" placeholder="填写处理备注,如:已发送邀请码、已回复邮件等">{$row.admin_remark|htmlentities}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
25
docs/toolsapi/application/admin/view/form_collect/index.html
Normal file
25
docs/toolsapi/application/admin/view/form_collect/index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<div class="panel panel-default panel-intro">
|
||||
{:build_heading()}
|
||||
<div class="panel-body">
|
||||
<div id="myTabContent" class="tab-content">
|
||||
<div class="tab-pane fade active in" id="one">
|
||||
<div class="widget-body no-padding">
|
||||
<div id="toolbar" class="toolbar">
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i></a>
|
||||
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('form_collect/add')?'':'hide'}" title="{:__('Add')}"><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('form_collect/edit')?'':'hide'}" title="{:__('Edit')}"><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('form_collect/del')?'':'hide'}" title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-batch-process btn-disabled disabled {:$auth->check('form_collect/batch_process')?'':'hide'}" title="批量标记已处理"><i class="fa fa-check-circle"></i> 批量处理</a>
|
||||
<a href="javascript:;" class="btn btn-info btn-export {:$auth->check('form_collect/export')?'':'hide'}" title="导出CSV"><i class="fa fa-download"></i> 导出</a>
|
||||
<a href="javascript:;" class="btn btn-warning btn-install {:$auth->check('form_collect/install')?'':'hide'}" title="安装数据表"><i class="fa fa-database"></i> 安装</a>
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="{:$auth->check('form_collect/edit')}"
|
||||
data-operate-del="{:$auth->check('form_collect/del')}"
|
||||
width="100%">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
323
docs/toolsapi/application/api/controller/FormCollect.php
Normal file
323
docs/toolsapi/application/api/controller/FormCollect.php
Normal file
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\controller\Api;
|
||||
use think\Db;
|
||||
use think\Cache;
|
||||
|
||||
/**
|
||||
* 表单收集 — 服务端API
|
||||
* 创建时间: 2026-06-15
|
||||
* 更新时间: 2026-06-15
|
||||
* 作用: 收集用户表单信息(邮箱等),支持多来源页面,保留扩展
|
||||
* 上次更新: v11.4.0 初始版本
|
||||
* 安全: 频率限制 + 邮箱验证 + XSS过滤
|
||||
*/
|
||||
class FormCollect extends Api
|
||||
{
|
||||
protected $noNeedLogin = ['submit', 'list', 'install'];
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
// 频率限制:每IP每分钟最多30次
|
||||
private $rateLimitMax = 30;
|
||||
private $rateLimitWindow = 60;
|
||||
|
||||
// 允许的来源标识
|
||||
private $allowedSources = [
|
||||
'app_gp_beta', // app.html Google Play内测
|
||||
'register_subscribe', // 注册页面订阅闲言邮箱
|
||||
'beta_questionnaire', // Beta页面问卷
|
||||
];
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-Id, Token');
|
||||
if ($this->request->method() === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
parent::_initialize();
|
||||
$this->checkRateLimit();
|
||||
}
|
||||
|
||||
// ─── 频率限制 ─────────────────────────────────────────
|
||||
|
||||
private function checkRateLimit()
|
||||
{
|
||||
$ip = $this->request->ip();
|
||||
$key = 'rate_limit_fc_' . md5($ip);
|
||||
$now = time();
|
||||
$requests = Cache::get($key, []);
|
||||
|
||||
$requests = array_filter($requests, function ($t) use ($now) {
|
||||
return ($now - $t) < $this->rateLimitWindow;
|
||||
});
|
||||
|
||||
if (count($requests) >= $this->rateLimitMax) {
|
||||
$this->error('请求过于频繁,请稍后再试', null, 429);
|
||||
}
|
||||
|
||||
$requests[] = $now;
|
||||
Cache::set($key, $requests, $this->rateLimitWindow);
|
||||
}
|
||||
|
||||
// ─── POST /submit — 提交表单 ─────────────────────────
|
||||
|
||||
/**
|
||||
* 提交表单信息
|
||||
* POST /api/form_collect/submit
|
||||
*
|
||||
* 请求参数:
|
||||
* - email: 邮箱地址(必填)
|
||||
* - source: 来源标识(必填,如 app_gp_beta/register_subscribe/beta_questionnaire)
|
||||
* - title: 表单标题(可选,若不传则根据source自动生成)
|
||||
* - extra_json: 扩展字段JSON(可选,预留后续收集其他信息)
|
||||
* - uid: 用户ID(可选,已登录用户)
|
||||
* - device_id: 设备ID(可选)
|
||||
*/
|
||||
public function submit()
|
||||
{
|
||||
$email = $this->request->post('email', '');
|
||||
$source = $this->request->post('source', '');
|
||||
$title = $this->request->post('title', '');
|
||||
$uid = $this->request->post('uid', '');
|
||||
$deviceId = $this->request->post('device_id', '');
|
||||
|
||||
// extra_json从原始输入提取,避免ThinkPHP对JSON请求体的类型检查报错
|
||||
$extraJson = '';
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$allParams = json_decode($rawInput, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && isset($allParams['extra_json'])) {
|
||||
// JSON请求体:直接提取,可能是对象/数组
|
||||
$extraJson = (is_array($allParams['extra_json']))
|
||||
? json_encode($allParams['extra_json'], JSON_UNESCAPED_UNICODE)
|
||||
: $allParams['extra_json'];
|
||||
} else {
|
||||
// form-urlencoded请求体:用parse_str提取
|
||||
parse_str($rawInput, $rawParams);
|
||||
if (isset($rawParams['extra_json'])) {
|
||||
$extraJson = $rawParams['extra_json'];
|
||||
}
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if (empty($email)) {
|
||||
$this->error('邮箱地址必填', null, 400);
|
||||
}
|
||||
|
||||
if (!$this->isValidEmail($email)) {
|
||||
$this->error('请输入有效的邮箱地址', null, 400);
|
||||
}
|
||||
|
||||
if (empty($source)) {
|
||||
$this->error('来源标识必填', null, 400);
|
||||
}
|
||||
|
||||
// 来源白名单校验(必须在XSS过滤之前,防止绕过)
|
||||
if (!in_array($source, $this->allowedSources)) {
|
||||
$this->error('无效的来源标识', null, 400);
|
||||
}
|
||||
|
||||
// 自动生成标题
|
||||
if (empty($title)) {
|
||||
$title = $this->generateTitle($source);
|
||||
}
|
||||
|
||||
// XSS过滤(extra_json单独处理,不转义引号)
|
||||
$email = htmlspecialchars(trim($email), ENT_QUOTES, 'UTF-8');
|
||||
$title = htmlspecialchars(trim($title), ENT_QUOTES, 'UTF-8');
|
||||
$source = htmlspecialchars(trim($source), ENT_QUOTES, 'UTF-8');
|
||||
$uid = htmlspecialchars(trim($uid), ENT_QUOTES, 'UTF-8');
|
||||
$deviceId = htmlspecialchars(trim($deviceId), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// 验证extra_json(在htmlspecialchars之前,避免引号被转义)
|
||||
if (!empty($extraJson)) {
|
||||
$decoded = json_decode($extraJson, true);
|
||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->error('extra_json格式错误', null, 400);
|
||||
}
|
||||
// 递归过滤XSS
|
||||
$decoded = $this->sanitizeArray($decoded);
|
||||
$extraJson = json_encode($decoded, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
// 检查重复提交(同邮箱+同来源24小时内)
|
||||
$duplicateKey = 'fc_dup_' . md5($email . '_' . $source);
|
||||
if (Cache::get($duplicateKey)) {
|
||||
$this->success('您已提交过,我们会尽快处理', [
|
||||
'already_submitted' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$ip = $this->request->ip();
|
||||
$now = time();
|
||||
|
||||
$record = [
|
||||
'email' => $email,
|
||||
'source' => $source,
|
||||
'title' => $title,
|
||||
'extra_json' => $extraJson,
|
||||
'uid' => $uid,
|
||||
'device_id' => $deviceId,
|
||||
'ip' => htmlspecialchars($ip, ENT_QUOTES, 'UTF-8'),
|
||||
'status' => 'pending',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
try {
|
||||
Db::name('form_collect')->insert($record);
|
||||
// 设置24小时防重复
|
||||
Cache::set($duplicateKey, 1, 86400);
|
||||
$this->success('提交成功', [
|
||||
'submitted' => true,
|
||||
'message' => '感谢您的提交,我们会尽快处理!',
|
||||
]);
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('提交失败: ' . $e->getMessage(), null, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GET /list — 查询提交记录 ─────────────────────────
|
||||
|
||||
/**
|
||||
* 查询表单提交记录(按来源或邮箱筛选)
|
||||
* GET /api/form_collect/list
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$source = $this->request->param('source', '');
|
||||
$email = $this->request->param('email', '');
|
||||
$page = max(intval($this->request->param('page', 1)), 1);
|
||||
$limit = min(max(intval($this->request->param('limit', 20)), 1), 50);
|
||||
|
||||
$query = Db::name('form_collect');
|
||||
|
||||
if (!empty($source)) {
|
||||
$query->where('source', $source);
|
||||
}
|
||||
if (!empty($email)) {
|
||||
$query->where('email', $email);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->order('id', 'desc')->page($page, $limit)->select();
|
||||
|
||||
// 脱敏邮箱
|
||||
foreach ($list as &$item) {
|
||||
$item['email_masked'] = $this->maskEmail($item['email']);
|
||||
}
|
||||
|
||||
$this->success('ok', [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── POST /install — 安装数据表 ────────────────────────
|
||||
|
||||
/**
|
||||
* 安装form_collect数据库表
|
||||
* POST /api/form_collect/install
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
try {
|
||||
$prefix = config('database.prefix') ?: 'tool_';
|
||||
$tableName = $prefix . 'form_collect';
|
||||
|
||||
$checkSql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '{$tableName}'";
|
||||
$exists = Db::query($checkSql);
|
||||
if (!empty($exists) && $exists[0]['COUNT(*)'] > 0) {
|
||||
$this->success('ok', ['table' => $tableName, 'created' => false, 'exists' => true]);
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "CREATE TABLE `{$tableName}` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(256) NOT NULL DEFAULT '' COMMENT '邮箱地址',
|
||||
`source` varchar(64) NOT NULL DEFAULT '' COMMENT '来源标识',
|
||||
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '表单标题(自动生成)',
|
||||
`extra_json` text COMMENT '扩展字段JSON',
|
||||
`uid` varchar(128) NOT NULL DEFAULT '' COMMENT '用户ID',
|
||||
`device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '设备ID',
|
||||
`ip` varchar(64) NOT NULL DEFAULT '' COMMENT '提交IP',
|
||||
`status` enum('pending','processed','invalid') NOT NULL DEFAULT 'pending' COMMENT '状态',
|
||||
`admin_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '管理员备注',
|
||||
`created_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_email` (`email`),
|
||||
KEY `idx_source` (`source`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表单收集记录'";
|
||||
|
||||
Db::execute($sql);
|
||||
$this->success('ok', ['table' => $tableName, 'created' => true]);
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('安装失败: ' . $e->getMessage(), null, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 内部方法 ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
*/
|
||||
private function isValidEmail($email)
|
||||
{
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据来源自动生成标题
|
||||
*/
|
||||
private function generateTitle($source)
|
||||
{
|
||||
$titleMap = [
|
||||
'app_gp_beta' => 'Google Play内测资格申请',
|
||||
'register_subscribe' => '闲言邮箱订阅',
|
||||
'beta_questionnaire' => 'Beta问卷-填写Gmail',
|
||||
];
|
||||
return isset($titleMap[$source]) ? $titleMap[$source] : '表单提交-' . $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱脱敏
|
||||
*/
|
||||
private function maskEmail($email)
|
||||
{
|
||||
$parts = explode('@', $email);
|
||||
if (count($parts) !== 2) return '***';
|
||||
$name = $parts[0];
|
||||
$domain = $parts[1];
|
||||
if (mb_strlen($name) <= 2) {
|
||||
return mb_substr($name, 0, 1) . '***@' . $domain;
|
||||
}
|
||||
return mb_substr($name, 0, 2) . '***@' . $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归过滤数组中的XSS
|
||||
*/
|
||||
private function sanitizeArray($arr)
|
||||
{
|
||||
if (!is_array($arr)) {
|
||||
return is_string($arr) ? htmlspecialchars($arr, ENT_QUOTES, 'UTF-8') : $arr;
|
||||
}
|
||||
foreach ($arr as $key => $val) {
|
||||
$arr[$key] = $this->sanitizeArray($val);
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -781,6 +781,13 @@ Route::rule([
|
||||
'api/font_sync/install' => 'api/FontSync/install',
|
||||
]);
|
||||
|
||||
// APP API路由 - 表单收集
|
||||
Route::rule([
|
||||
'api/form_collect/submit' => 'api/FormCollect/submit',
|
||||
'api/form_collect/list' => 'api/FormCollect/list',
|
||||
'api/form_collect/install' => 'api/FormCollect/install',
|
||||
]);
|
||||
|
||||
// APP API路由 - 插件更新
|
||||
Route::rule([
|
||||
'api/plugin_update/checkOne' => 'api/PluginUpdate/checkOne',
|
||||
|
||||
344
docs/toolsapi/docs/API_FORM_COLLECT_DOC.md
Normal file
344
docs/toolsapi/docs/API_FORM_COLLECT_DOC.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# 表单收集 API 文档
|
||||
|
||||
> @File: API_FORM_COLLECT_DOC.md
|
||||
> @Time: 2026-06-15
|
||||
> @Description: 表单收集接口文档 — 收集邮箱等信息,支持多来源页面,保留扩展
|
||||
> @LastUpdate: v11.4.0 初始版本
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
表单收集模块用于统一收集用户提交的表单信息(当前主要收集邮箱),支持多来源页面自动标识,预留扩展字段供后续收集其他信息。
|
||||
|
||||
### 1.1 基础信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 基础URL | `https://tools.wktyl.com/api/form_collect` |
|
||||
| 认证方式 | 无需登录 |
|
||||
| 响应格式 | JSON |
|
||||
| 编码 | UTF-8 |
|
||||
|
||||
### 1.2 来源标识(source)
|
||||
|
||||
不同页面提交时自动传入不同source,后台自动生成对应标题:
|
||||
|
||||
| source | 标题 | 来源页面 |
|
||||
|--------|------|----------|
|
||||
| app_gp_beta | Google Play内测资格申请 | app.html Google Play对话框 |
|
||||
| register_subscribe | 闲言邮箱订阅 | 注册页面 |
|
||||
| beta_questionnaire | Beta问卷-填写Gmail | Beta页面问卷 |
|
||||
|
||||
### 1.3 通用响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "提示信息",
|
||||
"time": "1718438400",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| code | 状态码,1=成功,0=失败 |
|
||||
| msg | 提示信息 |
|
||||
| data | 返回数据 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口列表
|
||||
|
||||
| 方法 | URL | 说明 |
|
||||
|------|-----|------|
|
||||
| POST | /api/form_collect/submit | 提交表单 |
|
||||
| GET | /api/form_collect/list | 查询提交记录 |
|
||||
| POST | /api/form_collect/install | 安装数据表 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 提交表单
|
||||
|
||||
**POST** `/api/form_collect/submit`
|
||||
|
||||
### 3.1 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| email | string | ✅ | 邮箱地址 |
|
||||
| source | string | ✅ | 来源标识(app_gp_beta/register_subscribe/beta_questionnaire) |
|
||||
| title | string | ❌ | 表单标题(不传则根据source自动生成) |
|
||||
| extra_json | string | ❌ | 扩展字段JSON(预留后续收集其他信息) |
|
||||
| uid | string | ❌ | 用户ID(已登录用户) |
|
||||
| device_id | string | ❌ | 设备ID |
|
||||
|
||||
### 3.2 请求示例
|
||||
|
||||
```bash
|
||||
# Google Play内测申请
|
||||
curl -X POST 'https://tools.wktyl.com/api/form_collect/submit' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d 'email=test@gmail.com&source=app_gp_beta'
|
||||
|
||||
# 注册页面订阅
|
||||
curl -X POST 'https://tools.wktyl.com/api/form_collect/submit' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d 'email=user@outlook.com&source=register_subscribe&uid=12345'
|
||||
|
||||
# Beta问卷(带扩展字段)
|
||||
curl -X POST 'https://tools.wktyl.com/api/form_collect/submit' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d 'email=beta@gmail.com&source=beta_questionnaire&extra_json={"device":"iPhone 16","os":"iOS 19"}'
|
||||
```
|
||||
|
||||
### 3.3 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "提交成功",
|
||||
"data": {
|
||||
"submitted": true,
|
||||
"message": "感谢您的提交,我们会尽快处理!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 重复提交响应
|
||||
|
||||
同邮箱+同来源24小时内重复提交:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "您已提交过,我们会尽快处理",
|
||||
"data": {
|
||||
"already_submitted": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 错误响应
|
||||
|
||||
| 场景 | code | msg |
|
||||
|------|------|-----|
|
||||
| 邮箱为空 | 0 | 邮箱地址必填 |
|
||||
| 邮箱格式错误 | 0 | 请输入有效的邮箱地址 |
|
||||
| 来源为空 | 0 | 来源标识必填 |
|
||||
| extra_json格式错误 | 0 | extra_json格式错误 |
|
||||
| 频率超限 | 0 | 请求过于频繁,请稍后再试 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 查询提交记录
|
||||
|
||||
**GET** `/api/form_collect/list`
|
||||
|
||||
### 4.1 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| source | string | ❌ | 按来源筛选 |
|
||||
| email | string | ❌ | 按邮箱筛选 |
|
||||
| page | int | ❌ | 页码(默认1) |
|
||||
| limit | int | ❌ | 每页条数(默认20,最大50) |
|
||||
|
||||
### 4.2 请求示例
|
||||
|
||||
```bash
|
||||
# 查询Google Play内测申请
|
||||
curl 'https://tools.wktyl.com/api/form_collect/list?source=app_gp_beta&page=1&limit=10'
|
||||
|
||||
# 按邮箱查询
|
||||
curl 'https://tools.wktyl.com/api/form_collect/list?email=test@gmail.com'
|
||||
```
|
||||
|
||||
### 4.3 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "ok",
|
||||
"data": {
|
||||
"total": 25,
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"email": "test@gmail.com",
|
||||
"email_masked": "te***@gmail.com",
|
||||
"source": "app_gp_beta",
|
||||
"title": "Google Play内测资格申请",
|
||||
"extra_json": "",
|
||||
"uid": "",
|
||||
"device_id": "",
|
||||
"ip": "1.2.3.4",
|
||||
"status": "pending",
|
||||
"admin_remark": "",
|
||||
"created_at": 1718438400,
|
||||
"updated_at": 1718438400
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 安装数据表
|
||||
|
||||
**POST** `/api/form_collect/install`
|
||||
|
||||
首次部署时调用,创建 `tool_form_collect` 表。
|
||||
|
||||
### 5.1 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://tools.wktyl.com/api/form_collect/install'
|
||||
```
|
||||
|
||||
### 5.2 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "ok",
|
||||
"data": {
|
||||
"table": "tool_form_collect",
|
||||
"created": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `tool_form_collect` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(256) NOT NULL DEFAULT '' COMMENT '邮箱地址',
|
||||
`source` varchar(64) NOT NULL DEFAULT '' COMMENT '来源标识',
|
||||
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '表单标题(自动生成)',
|
||||
`extra_json` text COMMENT '扩展字段JSON',
|
||||
`uid` varchar(128) NOT NULL DEFAULT '' COMMENT '用户ID',
|
||||
`device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '设备ID',
|
||||
`ip` varchar(64) NOT NULL DEFAULT '' COMMENT '提交IP',
|
||||
`status` enum('pending','processed','invalid') NOT NULL DEFAULT 'pending' COMMENT '状态',
|
||||
`admin_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '管理员备注',
|
||||
`created_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_email` (`email`),
|
||||
KEY `idx_source` (`source`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表单收集记录';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 后台管理
|
||||
|
||||
### 7.1 管理页面
|
||||
|
||||
**URL**: `https://tools.wktyl.com/admin.php/form_collect`
|
||||
|
||||
**菜单位置**: 表单收集 → 表单收集管理
|
||||
|
||||
### 7.2 后台功能
|
||||
|
||||
| 功能 | 端点 | 说明 |
|
||||
|------|------|------|
|
||||
| 列表 | GET /form_collect | 查看/搜索/筛选提交记录 |
|
||||
| 编辑 | POST /form_collect/edit | 修改状态、添加备注 |
|
||||
| 删除 | POST /form_collect/del | 删除记录 |
|
||||
| 导出 | GET /form_collect/export | 导出CSV |
|
||||
| 批量处理 | POST /form_collect/batch_process | 批量标记已处理 |
|
||||
| 统计 | GET /form_collect/stats | 各来源提交统计 |
|
||||
| 安装 | GET /form_collect/install | 安装数据表 |
|
||||
|
||||
### 7.3 后台菜单注册SQL
|
||||
|
||||
```sql
|
||||
INSERT INTO fa_auth_rule (type, pid, name, title, icon, condition, remark, ismenu, weigh, status)
|
||||
SELECT 'file', 0, 'form_collect', '表单收集', 'fa fa-wpforms', '', '管理表单收集记录', 1, 0, 'normal'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM fa_auth_rule WHERE name = 'form_collect');
|
||||
|
||||
SET @parent_id = (SELECT id FROM fa_auth_rule WHERE name = 'form_collect' LIMIT 1);
|
||||
|
||||
INSERT IGNORE INTO fa_auth_rule (type, pid, name, title, icon, ismenu, weigh, status) VALUES
|
||||
('file', @parent_id, 'form_collect/index', '查看', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/add', '添加', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/edit', '编辑', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/del', '删除', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/install', '安装', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/export', '导出', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/batch_process', '批量处理', '', 0, 0, 'normal'),
|
||||
('file', @parent_id, 'form_collect/stats', '统计', '', 0, 0, 'normal');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| 频率限制 | 每IP每分钟最多10次请求 |
|
||||
| 防重复 | 同邮箱+同来源24小时内不重复入库 |
|
||||
| XSS过滤 | 所有输入字段htmlspecialchars处理 |
|
||||
| 邮箱验证 | filter_var FILTER_VALIDATE_EMAIL |
|
||||
| CORS | 支持跨域请求 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 客户端对接
|
||||
|
||||
### 9.1 app.html — Google Play内测对话框
|
||||
|
||||
提交邮箱后调用API,成功后显示提交动画,不再跳转Google Forms。
|
||||
|
||||
```javascript
|
||||
fetch('https://tools.wktyl.com/api/form_collect/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'email=' + encodeURIComponent(email) + '&source=app_gp_beta'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 1) {
|
||||
// 显示提交成功动画
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 9.2 注册页面 — 订阅闲言邮箱
|
||||
|
||||
```javascript
|
||||
fetch('https://tools.wktyl.com/api/form_collect/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'email=' + encodeURIComponent(email) + '&source=register_subscribe&uid=' + uid
|
||||
});
|
||||
```
|
||||
|
||||
### 9.3 Beta页面 — 问卷填写Gmail
|
||||
|
||||
```javascript
|
||||
fetch('https://tools.wktyl.com/api/form_collect/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'email=' + encodeURIComponent(email) + '&source=beta_questionnaire&uid=' + uid
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 变更日志
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| v11.4.0 | 2026-06-15 | 初始版本:邮箱收集、多来源标识、后台管理、CSV导出 |
|
||||
180
docs/toolsapi/public/assets/js/backend/form_collect.js
Normal file
180
docs/toolsapi/public/assets/js/backend/form_collect.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 表单收集后台管理JS
|
||||
* 创建时间: 2026-06-15
|
||||
* 更新时间: 2026-06-15
|
||||
* 作用: 列表展示、添加/编辑弹窗、批量处理、导出CSV、标记已处理
|
||||
* 上次更新: 初始版本
|
||||
*/
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||
|
||||
var Controller = {
|
||||
index: function () {
|
||||
Table.api.init({
|
||||
extend: {
|
||||
index_url: 'form_collect/index' + location.search,
|
||||
add_url: 'form_collect/add',
|
||||
edit_url: 'form_collect/edit',
|
||||
del_url: 'form_collect/del',
|
||||
table: 'form_collect',
|
||||
}
|
||||
});
|
||||
|
||||
var table = $("#table");
|
||||
|
||||
table.bootstrapTable({
|
||||
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||||
pk: 'id',
|
||||
sortName: 'id',
|
||||
sortOrder: 'desc',
|
||||
columns: [
|
||||
[
|
||||
{checkbox: true},
|
||||
{field: 'id', title: 'ID', sortable: true},
|
||||
{field: 'email', title: __('Email'), operate: 'LIKE',
|
||||
formatter: function(val) {
|
||||
return '<span style="font-family:monospace">' + (val || '') + '</span>';
|
||||
}
|
||||
},
|
||||
{field: 'source', title: __('Source'),
|
||||
searchList: {
|
||||
'app_gp_beta': 'Google Play内测',
|
||||
'register_subscribe': '闲言邮箱订阅',
|
||||
'beta_questionnaire': 'Beta问卷'
|
||||
},
|
||||
formatter: function(val) {
|
||||
var map = {
|
||||
'app_gp_beta': '<span class="label label-primary">Google Play内测</span>',
|
||||
'register_subscribe': '<span class="label label-info">闲言邮箱订阅</span>',
|
||||
'beta_questionnaire': '<span class="label label-warning">Beta问卷</span>'
|
||||
};
|
||||
return map[val] || '<span class="label label-default">' + val + '</span>';
|
||||
}
|
||||
},
|
||||
{field: 'title', title: __('Title'), operate: 'LIKE',
|
||||
formatter: function(val) {
|
||||
return val || '-';
|
||||
}
|
||||
},
|
||||
{field: 'status', title: __('Status'),
|
||||
searchList: {
|
||||
'pending': '待处理',
|
||||
'processed': '已处理',
|
||||
'invalid': '无效'
|
||||
},
|
||||
formatter: function(val) {
|
||||
var map = {
|
||||
'pending': '<span class="label label-warning">⏳ 待处理</span>',
|
||||
'processed': '<span class="label label-success">✅ 已处理</span>',
|
||||
'invalid': '<span class="label label-default">❌ 无效</span>'
|
||||
};
|
||||
return map[val] || '<span class="label label-default">' + val + '</span>';
|
||||
}
|
||||
},
|
||||
{field: 'admin_remark', title: __('Admin_remark'), operate: 'LIKE',
|
||||
formatter: function(val) {
|
||||
if (!val) return '<span style="color:#999">-</span>';
|
||||
return val.length > 20 ? val.substr(0, 20) + '...' : val;
|
||||
}
|
||||
},
|
||||
{field: 'ip', title: __('Ip'), operate: 'LIKE',
|
||||
formatter: function(val) {
|
||||
return '<span style="font-family:monospace;font-size:12px">' + (val || '-') + '</span>';
|
||||
}
|
||||
},
|
||||
{field: 'created_at', title: __('Created_at'), sortable: true, operate: 'RANGE',
|
||||
addclass: 'datetimerange',
|
||||
formatter: function(val) {
|
||||
return val ? new Date(val * 1000).toLocaleString('zh-CN') : '-';
|
||||
}
|
||||
},
|
||||
{field: 'operate', title: __('Operate'), table: table,
|
||||
events: Table.api.events.operate,
|
||||
formatter: Table.api.formatter.operate,
|
||||
buttons: [
|
||||
{
|
||||
name: 'mark_processed',
|
||||
text: '标记已处理',
|
||||
title: '标记为已处理',
|
||||
classname: 'btn btn-xs btn-success btn-mark-processed',
|
||||
icon: 'fa fa-check',
|
||||
hidden: function(row) {
|
||||
return row.status === 'processed';
|
||||
},
|
||||
callback: function(data) {
|
||||
Layer.confirm('确认将此记录标记为已处理?', function(index) {
|
||||
Fast.api.ajax({
|
||||
url: 'form_collect/mark_processed',
|
||||
data: {ids: data.id}
|
||||
}, function(data, ret) {
|
||||
Layer.close(index);
|
||||
Toastr.success(ret.msg || '操作成功');
|
||||
$(".btn-refresh").trigger("click");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
|
||||
// 导出CSV按钮
|
||||
$(document).on('click', '.btn-export', function() {
|
||||
var ids = Table.api.selectedids(table);
|
||||
var source = table.bootstrapTable('getOptions').queryParams.source || '';
|
||||
var status = table.bootstrapTable('getOptions').queryParams.status || '';
|
||||
var url = 'form_collect/export?1=1';
|
||||
if (ids.length > 0) {
|
||||
url += '&ids=' + ids.join(',');
|
||||
}
|
||||
if (source) url += '&source=' + source;
|
||||
if (status) url += '&status=' + status;
|
||||
window.location.href = url;
|
||||
});
|
||||
|
||||
// 安装数据表按钮
|
||||
$(document).on('click', '.btn-install', function() {
|
||||
Layer.confirm('确认安装/初始化数据表?如果表已存在则不会重复创建。', function(index) {
|
||||
Fast.api.ajax('form_collect/install', function(data, ret) {
|
||||
Layer.close(index);
|
||||
Toastr.success(ret.msg || '安装成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 批量处理按钮
|
||||
$(document).on('click', '.btn-batch-process', function() {
|
||||
var ids = Table.api.selectedids(table);
|
||||
if (ids.length === 0) {
|
||||
Toastr.error('请先选择记录');
|
||||
return;
|
||||
}
|
||||
Layer.confirm('确认将选中的 ' + ids.length + ' 条记录标记为已处理?', function(index) {
|
||||
Fast.api.ajax({
|
||||
url: 'form_collect/batch_process',
|
||||
data: {ids: ids.join(',')}
|
||||
}, function(data, ret) {
|
||||
Layer.close(index);
|
||||
Toastr.success(ret.msg || '操作成功');
|
||||
$(".btn-refresh").trigger("click");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Table.api.bindevent(table);
|
||||
},
|
||||
add: function () {
|
||||
Controller.api.bindevent();
|
||||
},
|
||||
edit: function () {
|
||||
Controller.api.bindevent();
|
||||
},
|
||||
api: {
|
||||
bindevent: function () {
|
||||
Form.api.bindevent($("form[role=form]"));
|
||||
}
|
||||
}
|
||||
};
|
||||
return Controller;
|
||||
});
|
||||
Reference in New Issue
Block a user