文件
fenleixinxi/application/admin/command/FcInstall.php
MiAn d871e73d23 feat: 重构会员权益与分销配置,新增套餐维度权益管理
1. 新增会员权益配置控制器 Memberbenefit 和 Publish,拆分运营配置模块
2. 新增套餐表字段 free_view_quota、post_discount,支持按套餐配置免费看和发帖折扣
3. 优化前端页面,新增套餐编辑表单、区域选择器、发布配置交互
4. 新增调试支持,支持 mock openid、本地 IP 绕过权限,便于自动化测试
5. 新增数据库工具脚本,用于检查配置、修复数据、验证会员功能
6. 优化会员接口,返回套餐维度的权益信息,支持无限次免费看场景
2026-10-26 09:21:51 +08:00

1447 行
77 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\admin\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\Db;
/**
* 同城分类信息 · 业务表安装器
*
* 用法:
* php think fc:install 建表 + 首次注入种子数据(已存在则跳过种子)+ 轮播底色与春绿主题对齐
* php think fc:install --reset 建表 + 强制重置种子数据
*
* 说明:全部使用 CREATE TABLE IF NOT EXISTS,幂等安全,不影响 FastAdmin 原有表。
* 不加 --reset 时只补建表结构、对齐内置轮播底色,业务数据(发布/订单/会员)一律不动。
*/
class FcInstall extends Command
{
/**
* 内置轮播(标题 / 跳转 / 底色)——种子数据与「底色升级」共用同一份定义。
* color 取前端 uni.scss 的春绿主题(v4):105deg 亮绿(左)→ 深绿(右),
* 文字区落在亮绿端(小程序用墨绿字 #04301E),白色插画落在右侧深绿端;
* image 留空时小程序用「亮绿底 + 墨绿字 + 白色插画」内置配图渲染。
*/
const BANNERS = [
['同城好物节 · 二手直降', '/pages/post/list?categoryId=3', 'linear-gradient(120deg, #10b981 0%, #34d399 50%, #a7f3d0 100%)'],
['招聘旺季 · 免费发布', '/pages/post/create', 'linear-gradient(120deg, #059669 0%, #10b981 50%, #6ee7b7 100%)'],
['本地服务 · 上门更省心', '/pages/post/list?categoryId=5', 'linear-gradient(120deg, #00d26a 0%, #34d399 50%, #bbf7d0 100%)'],
];
/**
* 需要被主题升级覆盖的历史底色特征:
* ① 旧暖橙纯色(#FF8A5B 一族);② v1.2 生态绿(#0e7c66 一族);
* 命中任一即按标题覆盖为上面的春绿渐变(只动 color 字段,不动业务数据)。
*/
const LEGACY_COLOR_KEYS = [
'18a587', '0e7c66', '0a5d4d', '04302a', '16897f', '0e6b62', '07443e',
'ff8a5b', 'ffb86b', 'ff6f61',
];
protected function configure()
{
$this->setName('fc:install')
->addOption('reset', null, Option::VALUE_NONE, '强制重置种子数据')
->setDescription('同城分类信息业务表安装 + 种子数据');
}
protected function execute(Input $input, Output $output)
{
$prefix = \think\Config::get('database.prefix');
$reset = $input->hasOption('reset') && $input->getOption('reset');
$output->writeln('==> 建表(前缀 ' . $prefix . ')');
$tables = $this->tables();
foreach ($tables as $name => $sql) {
// 占位符 #P# 位于反引号内(形如 `#P#config`),故按裸串替换
Db::execute(str_replace('#P#', $prefix . 'fc_', $sql));
$output->writeln(' - ' . $prefix . 'fc_' . $name);
}
$seeded = Db::name('fc_config')->count();
if ($reset) {
$this->truncateAll();
$seeded = 0;
}
if (!$seeded) {
$output->writeln('==> 注入种子数据');
$this->seed($output);
} else {
$output->writeln('==> 已有数据,跳过种子(如需重置请加 --reset)');
}
$output->writeln('==> 轮播底色与春绿主题对齐');
$this->upgradeBanners($output);
$output->writeln('==> 发布模式与发布套餐升级');
$this->upgradePublish($output);
$output->writeln('==> 系统配置分组整理');
$this->upgradeConfigGroups($output);
$output->writeln('==> LBS / 店铺装修 / 支付 / 优惠券 / 举报升级(v1.7.0)');
$this->upgradeV170($output);
$output->writeln('==> 提现商家转账字段升级(v1.2.1)');
$this->upgradeV121($output);
$output->writeln('==> 优惠券抵扣 / 佣金结算字段升级(v1.2.2)');
$this->upgradeV122($output);
$output->writeln('==> 服务区域平铺升级(省 / 市 / 区县)');
$this->upgradeArea($output);
$output->writeln('==> 初始化后台菜单');
$this->menu($output);
$output->writeln('==> 完成');
}
/**
* 轮播底色升级(幂等,可重复执行)
*
* 主题迭代史:v1.0.x 暖橙纯色(#FF8A5B 一族)→ v1.2 生态绿(#0e7c66 一族)→ v1.3 春绿(本次)。
* 线上库若还是历史值,这里按「标题」精确匹配把内置 3 条轮播的底色覆盖为春绿渐变;
* 只动 color 字段,不新增/删除记录,也不影响发布、订单等业务数据。
* 判定规则:color 为空/非渐变(旧纯色),或命中 LEGACY_COLOR_KEYS(上一版主题色)→ 覆盖;
* 自定义过标题、或已是春绿渐变(#00e070 一族)的轮播不会被改动(幂等)。
*/
private function upgradeBanners(Output $output)
{
// ① 先确保 color 列容得下 CSS 渐变字符串:旧库是 varchar(16),只够存 #RRGGBB,
// 直接写 linear-gradient(...) 会被截断(严格模式还会报 1406 Data too long)。
$table = \think\Config::get('database.prefix') . 'fc_banner';
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE 'color'");
$row = isset($cols[0]) ? $cols[0] : [];
$type = isset($row['Type']) ? (string)$row['Type'] : (isset($row['type']) ? (string)$row['type'] : '');
if (preg_match('/^varchar\((\d+)\)$/i', $type, $m) && (int)$m[1] < 255) {
Db::execute("ALTER TABLE `{$table}` MODIFY COLUMN `color` varchar(255) NOT NULL DEFAULT ''"
. " COMMENT '底色:CSS 渐变 linear-gradient(...),留空则用内置春绿渐变'");
$output->writeln(' - 轮播底色列 color 已由 ' . $type . ' 扩容为 varchar(255)(存 CSS 渐变)');
}
// ② 历史底色(旧纯色 / 上一版生态绿)→ 春绿渐变
$upgraded = 0;
foreach (self::BANNERS as $b) {
$row = Db::name('fc_banner')->where('title', $b[0])->find();
if (!$row) {
continue;
}
$color = strtolower((string)$row['color']);
$isGradient = strpos($color, 'gradient(') !== false;
$isLegacy = false;
foreach (self::LEGACY_COLOR_KEYS as $k) {
if (strpos($color, $k) !== false) {
$isLegacy = true;
break;
}
}
if (!$isGradient || $isLegacy) {
Db::name('fc_banner')->where('id', $row['id'])
->update(['color' => $b[2], 'updatetime' => time()]);
$upgraded++;
}
}
$output->writeln($upgraded
? ' - 已把 ' . $upgraded . ' 条内置轮播底色升级为春绿渐变(105deg 亮绿→深绿)'
: ' - 轮播底色已是春绿渐变或非内置轮播,无需处理');
}
/**
* 发布模式与发布套餐升级(幂等,可重复执行)
*
* 背景:v1.0.7 新增「免费发布 / 付费发布」开关与付费发布定价
* · 单条价格 post.singlePrice
* · 免费发布条数 post.freeQuota / post.freeQuotaCycle(v1.0.10:达到条数后按单条价格计费)
* 套餐价格与含发布条数唯一真源 = fc_plan(后台「运营推广 → 会员套餐」维护),
* 不再使用 post.monthPrice / post.yearPrice 等配置键(历史键在本步骤清理)。
* 老库缺列/缺配置时补齐,**不覆盖运营已改过的值**:
* ① fc_user_ext 补 post_quota_left / post_quota_expire(剩余发布条数 + 有效期)
* ② fc_plan 补 post_quota(套餐含发布条数,月/年卡赠送额度)
* ③ fc_config 补 post.* 四项(已存在则跳过)
* ④ 清理历史遗留的套餐定价配置键(post.month* / post.year* / fee.*)
*/
private function upgradePublish(Output $output)
{
$prefix = \think\Config::get('database.prefix');
$changed = 0;
// ① 用户扩展表:发布额度
$extTable = $prefix . 'fc_user_ext';
if (!$this->hasColumn($extTable, 'post_quota_left')) {
Db::execute("ALTER TABLE `{$extTable}` ADD COLUMN `post_quota_left` int(10) NOT NULL DEFAULT 0"
. " COMMENT '剩余发布条数(月/年套餐赠送)' AFTER `free_view_used`");
$output->writeln(' - ' . $extTable . ' 新增列 post_quota_left');
$changed++;
}
if (!$this->hasColumn($extTable, 'post_quota_expire')) {
Db::execute("ALTER TABLE `{$extTable}` ADD COLUMN `post_quota_expire` int(10) NULL"
. " COMMENT '发布额度有效期(=会员到期时间)' AFTER `post_quota_left`");
$output->writeln(' - ' . $extTable . ' 新增列 post_quota_expire');
$changed++;
}
// ② 会员套餐:含发布条数、每日免费看条数、超额发布折扣(直接挂在每个套餐上)
$planTable = $prefix . 'fc_plan';
if (!$this->hasColumn($planTable, 'post_quota')) {
Db::execute("ALTER TABLE `{$planTable}` ADD COLUMN `post_quota` int(10) NOT NULL DEFAULT 0"
. " COMMENT '含发布条数(套餐赠送额度)' AFTER `days`");
$output->writeln(' - ' . $planTable . ' 新增列 post_quota');
$changed++;
}
if (!$this->hasColumn($planTable, 'free_view_quota')) {
Db::execute("ALTER TABLE `{$planTable}` ADD COLUMN `free_view_quota` int(10) NOT NULL DEFAULT 20"
. " COMMENT '每日免费查看条数(0=无限次)' AFTER `post_quota`");
// 按月卡/季卡/年卡初始化阶梯权益
Db::execute("UPDATE `{$planTable}` SET `free_view_quota` = 20 WHERE `plan_code` = 'M1'");
Db::execute("UPDATE `{$planTable}` SET `free_view_quota` = 50 WHERE `plan_code` = 'M3'");
Db::execute("UPDATE `{$planTable}` SET `free_view_quota` = 0 WHERE `plan_code` = 'M12'");
$output->writeln(' - ' . $planTable . ' 新增列 free_view_quota(0=无限次免费看)');
$changed++;
}
if (!$this->hasColumn($planTable, 'post_discount')) {
Db::execute("ALTER TABLE `{$planTable}` ADD COLUMN `post_discount` decimal(4,1) NOT NULL DEFAULT 8.0"
. " COMMENT '超额单条发布折扣(0.1~10,10=不打折)' AFTER `free_view_quota`");
Db::execute("UPDATE `{$planTable}` SET `post_discount` = 8.5 WHERE `plan_code` = 'M1'");
Db::execute("UPDATE `{$planTable}` SET `post_discount` = 8.0 WHERE `plan_code` = 'M3'");
Db::execute("UPDATE `{$planTable}` SET `post_discount` = 7.0 WHERE `plan_code` = 'M12'");
$output->writeln(' - ' . $planTable . ' 新增列 post_discount');
$changed++;
}
if (!$this->hasColumn($extTable, 'free_view_quota')) {
Db::execute("ALTER TABLE `{$extTable}` ADD COLUMN `free_view_quota` int(10) NOT NULL DEFAULT 20"
. " COMMENT '当前会员每日免费查看条数(0=无限)' AFTER `free_view_used`");
$output->writeln(' - ' . $extTable . ' 新增列 free_view_quota');
$changed++;
}
if (!$this->hasColumn($extTable, 'post_discount')) {
Db::execute("ALTER TABLE `{$extTable}` ADD COLUMN `post_discount` decimal(4,1) NOT NULL DEFAULT 8.0"
. " COMMENT '当前会员超额发布折扣(0.1~10,10=不打折)' AFTER `post_quota_expire`");
$output->writeln(' - ' . $extTable . ' 新增列 post_discount');
$changed++;
}
// ③ 配置项(只补缺失的,不覆盖运营已改的值)
$defaults = [
['post.mode', 'free', 'post', '发布模式:free 免费发布 / paid 付费发布'],
['post.singlePrice', '5', 'post', '单条发布价格(元)=付费发布标准价'],
['post.freeQuota', '0', 'post', '免费发布条数(0 = 不限制/免费到底;达到条数后按单条发布价格计费)'],
['post.freeQuotaCycle', 'day', 'post', '免费条数统计周期:day 每日 / month 每月 / total 累计'],
];
$now = time();
$added = [];
foreach ($defaults as $d) {
if (Db::name('fc_config')->where('key', $d[0])->find()) {
continue;
}
Db::name('fc_config')->insert([
'key' => $d[0], 'value' => $d[1], 'group_key' => $d[2], 'remark' => $d[3],
'createtime' => $now, 'updatetime' => $now,
]);
$added[] = $d[0];
}
if ($added) {
$output->writeln(' - 新增配置 ' . count($added) . ' 项:' . implode(' / ', $added));
$changed++;
}
// ④ 清理历史遗留配置键:套餐定价已统一到 fc_plan,发布免费条数只认 post.freeQuota
$deprecated = [
'post.monthPrice', 'post.monthQuota', 'post.yearPrice', 'post.yearQuota',
'fee.singlePost', 'fee.freePostPerDay', 'fee.memberMonth', 'fee.memberQuarter', 'fee.memberYear',
];
$removedKeys = Db::name('fc_config')->where('key', 'in', $deprecated)->delete();
if ($removedKeys) {
$output->writeln(' - 清理废弃配置 ' . $removedKeys . ' 项(套餐定价真源已统一为 fc_plan)');
$changed++;
}
$output->writeln($changed
? ' - 完成(发布模式 ' . $this->cfgValue('post.mode', 'free') . ')'
: ' - 发布模式与套餐配置已是最新,无需处理');
}
/**
* 系统配置分组整理(幂等)
*
* 背景:fa_config 的 configgroup 字典保留了 FastAdmin 演示分组 user(会员配置)与
* example(示例分组),两者没有任何配置项,却会在「系统配置」页渲染出空白 tab,
* 且 user 与业务分组「会员配置」重名易混淆;这里从字典中移除。
*/
private function upgradeConfigGroups(Output $output)
{
$row = Db::name('config')->where('name', 'configgroup')->find();
if (!$row) {
$output->writeln(' - 未找到 configgroup 字典,跳过');
return;
}
$decoded = json_decode((string)$row['value'], true);
if (!is_array($decoded)) {
$output->writeln(' - configgroup 字典格式异常,跳过');
return;
}
$target = ['basic', 'email', 'dictionary'];
$kept = [];
foreach ($decoded as $k => $v) {
if (in_array($k, $target, true) || strpos((string)$k, 'fc_') === 0) {
$kept[$k] = $v;
}
}
if ($kept === $decoded) {
$output->writeln(' - 系统配置分组已是最新,无需处理');
return;
}
Db::name('config')->where('name', 'configgroup')->update([
'value' => json_encode($kept, JSON_UNESCAPED_UNICODE),
]);
$removed = array_diff(array_keys($decoded), array_keys($kept));
$output->writeln(' - 已移除空分组:' . implode(' / ', $removed));
}
/**
* v1.7.0 升级:LBS 经纬度 / 店铺装修字段 / 收藏关注 / 支付与举报配置(幂等,可重复执行)
*
* 背景:本轮新增 ① 首页「按距离排序」与详情页真实地图 → fc_post 存经纬度;
* ② 店铺装修与推荐位 → fc_merchant 增 banner / notice / recommend;
* ③ 微信支付 JSAPI v2 与优惠券、举报原因字典 → 新增 fc_config 配置项。
* 规则:列「不存在才 ADD COLUMN」、配置「已存在则跳过」,**绝不覆盖运营已改过的值**。
* 新增业务表(fc_favorite / fc_follow / fc_coupon / fc_user_coupon)在上方的 tables() 中
* 以 CREATE TABLE IF NOT EXISTS 建立。
*/
private function upgradeV170(Output $output)
{
$prefix = \think\Config::get('database.prefix');
$changed = 0;
// ① fc_post:发布坐标(gcj02),供首页按距离排序与详情页地图使用
$postTable = $prefix . 'fc_post';
$postCols = [
['lat', 'decimal(10,6) NULL COMMENT \'纬度 gcj02(-90~90),未定位为空\'', '`address`'],
['lng', 'decimal(10,6) NULL COMMENT \'经度 gcj02(-180~180),未定位为空\'', '`lat`'],
];
foreach ($postCols as $col) {
if ($this->hasColumn($postTable, $col[0])) {
continue;
}
Db::execute("ALTER TABLE `{$postTable}` ADD COLUMN `{$col[0]}` {$col[1]} AFTER {$col[2]}");
$output->writeln(' - ' . $postTable . ' 新增列 ' . $col[0]);
$changed++;
}
// ② fc_merchant:店铺头图 / 店铺公告 / 推荐位 / 实体店地址与经纬度
$merchantTable = $prefix . 'fc_merchant';
$merchantCols = [
['address', "varchar(255) NOT NULL DEFAULT '' COMMENT '实体店详细地址'", '`contact`'],
['lat', 'decimal(10,6) NULL COMMENT \'纬度 gcj02\'', '`address`'],
['lng', 'decimal(10,6) NULL COMMENT \'经度 gcj02\'', '`lat`'],
['banner', "varchar(255) NOT NULL DEFAULT '' COMMENT '店铺头图'", '`logo`'],
['notice', "varchar(255) NOT NULL DEFAULT '' COMMENT '店铺公告'", '`banner`'],
['recommend', "tinyint(1) NOT NULL DEFAULT 0 COMMENT '推荐位:1 推荐 0 普通'", '`notice`'],
];
foreach ($merchantCols as $col) {
if ($this->hasColumn($merchantTable, $col[0])) {
continue;
}
Db::execute("ALTER TABLE `{$merchantTable}` ADD COLUMN `{$col[0]}` {$col[1]} AFTER {$col[2]}");
$output->writeln(' - ' . $merchantTable . ' 新增列 ' . $col[0]);
$changed++;
}
// ③ 配置项(只补缺失的,不覆盖运营已改的值)
// 注意:**不再写入支付配置**(历史键 wx.mchId / wx.apiKey / wx.payNotifyUrl 已废弃)。
// 微信支付参数由 epay 插件维护:后台「插件管理 → 微信支付宝整合插件 → 配置」,
// 持久化在 addons/epay/config.php(wechat.miniapp_id / wechat.mch_id / wechat.key)。
$defaults = [
['coupon.enabled', '1', 'coupon', '优惠券功能开关:1 开启 / 0 关闭'],
['report.reasons', json_encode(['虚假信息', '违法违禁', '垃圾广告', '重复发布', '其他'], JSON_UNESCAPED_UNICODE), 'report', '举报原因字典(JSON 数组)'],
];
$now = time();
$added = [];
foreach ($defaults as $d) {
if (Db::name('fc_config')->where('key', $d[0])->find()) {
continue;
}
Db::name('fc_config')->insert([
'key' => $d[0], 'value' => $d[1], 'group_key' => $d[2], 'remark' => $d[3],
'createtime' => $now, 'updatetime' => $now,
]);
$added[] = $d[0];
$changed++;
}
if ($added) {
$output->writeln(' - 新增配置 ' . count($added) . ' 项:' . implode(' / ', $added));
}
// ④ 插件钩子缓存:`application/extra/addons.php` 的 hooks 会被 File 缓存(Cache::get('hooks')),
// 若该缓存是在「插件目录还不存在」时生成的,插件启用后也不会被加载 ——
// 典型症状:微信支付报 `Class 'Yansongda\Pay\Pay' not found`。
// 这里主动清一次,保证「插件装好/更新后即刻可用」。
try {
\think\Cache::rm('hooks');
$output->writeln(' - 已清理插件钩子缓存 hooks(epay 插件即时生效)');
} catch (\Exception $e) {
// 清理失败不影响主流程:下一次请求会按 extra/addons.php 重建
}
$output->writeln($changed
? ' - 完成(新增结构 ' . $changed . ' 处)'
: ' - LBS / 店铺装修 / 支付 / 优惠券 / 举报配置已是最新,无需处理');
}
/**
* v1.2.1:提现「商家转账」字段
* - fa_fc_withdraw:out_bill_no / transfer_status / transfer_id / package_info / fail_reason / transfer_time
* - fa_fc_user_ext:realname(收款人真实姓名)
* 幂等:已存在则跳过
*/
private function upgradeV121(Output $output)
{
$prefix = \think\Config::get('database.prefix');
$w = $prefix . 'fc_withdraw';
$e = $prefix . 'fc_user_ext';
$cols = [
'out_bill_no' => "ALTER TABLE `{$w}` ADD COLUMN `out_bill_no` varchar(32) NOT NULL DEFAULT '' COMMENT '微信商家转账商户单号'",
'transfer_status' => "ALTER TABLE `{$w}` ADD COLUMN `transfer_status` varchar(16) NOT NULL DEFAULT '' COMMENT 'WAIT_USER_CONFIRM/PROCESSING/SUCCESS/FAIL/CANCELLED'",
'transfer_id' => "ALTER TABLE `{$w}` ADD COLUMN `transfer_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信转账单号'",
'package_info' => "ALTER TABLE `{$w}` ADD COLUMN `package_info` varchar(512) NOT NULL DEFAULT '' COMMENT '拉起确认收款页的package'",
'fail_reason' => "ALTER TABLE `{$w}` ADD COLUMN `fail_reason` varchar(255) NOT NULL DEFAULT ''",
'transfer_time' => "ALTER TABLE `{$w}` ADD COLUMN `transfer_time` int(10) DEFAULT NULL",
];
$changed = 0;
foreach ($cols as $col => $sql) {
if ($this->hasColumn($w, $col)) {
continue;
}
Db::execute($sql);
$changed++;
}
try {
Db::execute("ALTER TABLE `{$w}` ADD KEY `out_bill_no` (`out_bill_no`)");
$changed++;
} catch (\Exception $e2) {
// 索引已存在则忽略
}
if (!$this->hasColumn($e, 'realname')) {
Db::execute("ALTER TABLE `{$e}` ADD COLUMN `realname` varchar(32) NOT NULL DEFAULT '' COMMENT '收款人真实姓名(提现用)'");
$changed++;
}
// 退款单号(微信退款结果回调按此匹配订单)
$o = $prefix . 'fc_order';
if (!$this->hasColumn($o, 'out_refund_no')) {
Db::execute("ALTER TABLE `{$o}` ADD COLUMN `out_refund_no` varchar(64) NOT NULL DEFAULT '' COMMENT '微信退款单号(商户侧)'");
$changed++;
}
$output->writeln($changed
? ' - 完成(新增结构 ' . $changed . ' 处)'
: ' - 提现转账字段已是最新,无需处理');
}
/**
* 服务区域平铺升级(省 / 市 / 区县三列,不再使用层级)
*
* 目标结构:fc_area(id, province, city, district, lat, lng, sort, enabled)
* 每条记录 = 一个服务区域(区县);省 / 市只是该行的归属字段,供前端级联。
* 历史层级数据会自动压平:每个区县保留原 ID,省 / 市节点删除。
* 幂等:可重复执行。
*/
private function upgradeArea(Output $output)
{
$prefix = \think\Config::get('database.prefix');
$areaTable = $prefix . 'fc_area';
$postTable = $prefix . 'fc_post';
$draftTable = $prefix . 'fc_draft';
$now = time();
$changed = 0;
// ① fc_area 平铺字段
$areaCols = [
'province' => "ALTER TABLE `{$areaTable}` ADD COLUMN `province` varchar(32) NOT NULL DEFAULT '' COMMENT '省份' AFTER `id`",
'city' => "ALTER TABLE `{$areaTable}` ADD COLUMN `city` varchar(32) NOT NULL DEFAULT '' COMMENT '城市' AFTER `province`",
'district' => "ALTER TABLE `{$areaTable}` ADD COLUMN `district` varchar(32) NOT NULL DEFAULT '' COMMENT '区县(最终服务区域)' AFTER `city`",
'lat' => "ALTER TABLE `{$areaTable}` ADD COLUMN `lat` decimal(10,6) DEFAULT NULL COMMENT '中心纬度(gcj02)' AFTER `district`",
'lng' => "ALTER TABLE `{$areaTable}` ADD COLUMN `lng` decimal(10,6) DEFAULT NULL COMMENT '中心经度(gcj02)' AFTER `lat`",
];
foreach ($areaCols as $col => $sql) {
if ($this->hasColumn($areaTable, $col)) {
continue;
}
Db::execute($sql);
$changed++;
}
if (!$this->hasIndex($areaTable, 'province_city')) {
Db::execute("ALTER TABLE `{$areaTable}` ADD KEY `province_city` (`province`,`city`)");
}
// ② 旧层级数据压平:区县保留原 ID,省 / 市节点删除
$hasLevel = $this->hasColumn($areaTable, 'level');
$hasParent = $this->hasColumn($areaTable, 'parent_id');
$hasName = $this->hasColumn($areaTable, 'name');
if ($hasLevel || $hasParent || $hasName) {
$rows = Db::name('fc_area')->select();
$byId = [];
foreach ($rows as $row) {
$byId[(int)$row['id']] = $row;
}
$flat = 0;
foreach ($rows as $row) {
$level = $hasLevel ? (int)$row['level'] : 0;
// 只保留区县(level=3);真实省份/城市节点稍后删除
if ($hasLevel && $level > 0 && $level < 3) {
continue;
}
$provinceName = '';
$cityName = '';
$districtName = $hasName ? (string)$row['name'] : (string)$row['district'];
if ($hasParent && (int)$row['parent_id'] > 0 && isset($byId[(int)$row['parent_id']])) {
$cityRow = $byId[(int)$row['parent_id']];
$cityName = $hasName ? (string)$cityRow['name'] : (string)$cityRow['city'];
if ((int)$cityRow['parent_id'] > 0 && isset($byId[(int)$cityRow['parent_id']])) {
$provinceRow = $byId[(int)$cityRow['parent_id']];
$provinceName = $hasName ? (string)$provinceRow['name'] : (string)$provinceRow['province'];
}
}
Db::name('fc_area')->where('id', $row['id'])->update([
'province' => $provinceName,
'city' => $cityName,
'district' => $districtName,
'updatetime' => $now,
]);
$flat++;
}
// 删除省 / 市节点(它们不是服务区域)
if ($hasLevel) {
$deleted = Db::name('fc_area')->where('level', 'in', [1, 2])->delete();
}
// 删除旧字段,彻底去掉层级概念
foreach (['parent_id', 'level', 'name'] as $col) {
if ($this->hasColumn($areaTable, $col)) {
Db::execute("ALTER TABLE `{$areaTable}` DROP COLUMN `{$col}`");
}
}
$output->writeln(' - 已将 ' . $flat . ' 条区域压平为「省 / 市 / 区县」三列');
}
// ③ 已知区县中心坐标(仅补空值)
$coords = [
'云岩区' => [26.605, 106.710],
'南明区' => [26.568, 106.716],
'观山湖区' => [26.625, 106.625],
'花溪区' => [26.430, 106.670],
'白云区' => [26.678, 106.623],
];
foreach ($coords as $name => $ll) {
Db::name('fc_area')->where('district', $name)->whereNull('lat')->update([
'lat' => $ll[0], 'lng' => $ll[1], 'updatetime' => $now,
]);
}
// ④ fc_post:保留 district_id 作为服务区域ID,移除省/市 ID 列
if (!$this->hasColumn($postTable, 'district_id')) {
Db::execute("ALTER TABLE `{$postTable}` ADD COLUMN `district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)' AFTER `area`");
$changed++;
}
foreach (['province_id', 'city_id'] as $col) {
if ($this->hasColumn($postTable, $col)) {
Db::execute("ALTER TABLE `{$postTable}` DROP COLUMN `{$col}`");
}
}
if (!$this->hasIndex($postTable, 'district_id')) {
Db::execute("ALTER TABLE `{$postTable}` ADD KEY `district_id` (`district_id`)");
}
// ⑤ fc_draft:同样只保留 area + district_id
foreach (['area', 'district_id'] as $col) {
if ($this->hasColumn($draftTable, $col)) {
continue;
}
$sql = $col === 'area'
? "ALTER TABLE `{$draftTable}` ADD COLUMN `area` varchar(32) NOT NULL DEFAULT '' COMMENT '区县名称' AFTER `address`"
: "ALTER TABLE `{$draftTable}` ADD COLUMN `district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)' AFTER `area`";
Db::execute($sql);
$changed++;
}
foreach (['province_id', 'city_id'] as $col) {
if ($this->hasColumn($draftTable, $col)) {
Db::execute("ALTER TABLE `{$draftTable}` DROP COLUMN `{$col}`");
}
}
// ⑥ 按区县名称回填 district_id(信息 / 草稿)
$byName = [];
foreach (Db::name('fc_area')->select() as $area) {
$byName[(string)$area['district']] = (int)$area['id'];
}
$backfilled = 0;
foreach ([['fc_post', 'updatetime'], ['fc_draft', null]] as $t) {
$rows = Db::name($t[0])->where('district_id', 0)->where('area', '<>', '')->field('id,area')->select();
foreach ($rows as $row) {
if (!isset($byName[$row['area']])) {
continue;
}
$data = ['district_id' => $byName[$row['area']]];
if ($t[1]) {
$data[$t[1]] = $now;
}
Db::name($t[0])->where('id', $row['id'])->update($data);
$backfilled++;
}
}
if ($backfilled) {
$output->writeln(' - 已回填 ' . $backfilled . ' 条信息/草稿的服务区域ID');
}
$output->writeln($changed || $backfilled
? ' - 完成(结构变更 ' . $changed . ' 处,回填 ' . $backfilled . ' 条)'
: ' - 服务区域已是平铺结构,无需处理');
}
/** 表是否存在某列 */
private function hasColumn($table, $column)
{
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '" . addslashes($column) . "'");
return !empty($cols);
}
/**
* v1.2.2:优惠券抵扣 + 佣金结算周期
* - fa_fc_order:origin_amount / coupon_amount / coupon_id
* - fa_fc_commission:settle_due(预计结算时间)
*/
private function upgradeV122(Output $output)
{
$prefix = \think\Config::get('database.prefix');
$o = $prefix . 'fc_order';
$c = $prefix . 'fc_commission';
$changed = 0;
$orderCols = [
'origin_amount' => "ALTER TABLE `{$o}` ADD COLUMN `origin_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠前金额'",
'coupon_amount' => "ALTER TABLE `{$o}` ADD COLUMN `coupon_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠券抵扣金额'",
'coupon_id' => "ALTER TABLE `{$o}` ADD COLUMN `coupon_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用的用户优惠券ID'",
];
foreach ($orderCols as $col => $sql) {
if (!$this->hasColumn($o, $col)) {
Db::execute($sql);
$changed++;
}
}
if (!$this->hasColumn($c, 'settle_due')) {
Db::execute("ALTER TABLE `{$c}` ADD COLUMN `settle_due` int(10) DEFAULT NULL COMMENT '预计结算时间'");
$changed++;
}
$output->writeln($changed
? ' - 完成(新增结构 ' . $changed . ' 处)'
: ' - 优惠券/结算字段已是最新,无需处理');
}
/** 表是否存在某个索引 */
private function hasIndex($table, $index)
{
$rows = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = '" . addslashes($index) . "'");
return !empty($rows);
}
/** 读配置默认值(不依赖 api 基类的 cfg 缓存) */
private function cfgValue($key, $default = null)
{
$row = Db::name('fc_config')->where('key', $key)->find();
if (!$row || $row['value'] === '' || $row['value'] === null) {
return $default;
}
return $row['value'];
}
/**
* 初始化 FastAdmin 后台菜单(幂等)
*
* 菜单结构统一定义在 FcMenu::GROUPS / FcMenu::ITEMS,
* 保证 fc:install 与 fc:menu 产出同一套菜单。
*/
protected function menu(Output $output)
{
FcMenu::apply($output);
}
/** 清空业务表(保留表结构) */
protected function truncateAll()
{
foreach (array_keys($this->tables()) as $name) {
Db::execute('TRUNCATE TABLE `' . \think\Config::get('database.prefix') . 'fc_' . $name . '`');
}
}
/** 表结构定义:key 为表名后缀,SQL 中 `#P#` 会被替换为 prefix . 'fc_' */
protected function tables()
{
return [
'wallet_flow' => "CREATE TABLE IF NOT EXISTS `#P#wallet_flow` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`flow_no` varchar(32) NOT NULL DEFAULT '' COMMENT '流水号',
`type` varchar(10) NOT NULL DEFAULT '' COMMENT 'income=入账 expense=支出(用户钱包口径)',
`scene` varchar(32) NOT NULL DEFAULT '' COMMENT '场景:余额充值/后台充值/会员开通/发布付费/付费查看/订单退款(余额)/佣金提现',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变动金额',
`balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变动后钱包余额(fa_user.money)',
`related_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '关联单据ID(订单/信息/提现单)',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `flow_no` (`flow_no`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-用户钱包流水';",
'config' => "CREATE TABLE IF NOT EXISTS `#P#config` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(64) NOT NULL DEFAULT '' COMMENT '配置键',
`value` text COMMENT '配置值',
`group_key` varchar(32) NOT NULL DEFAULT '' COMMENT '分组',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '说明',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-业务配置';",
'category' => "CREATE TABLE IF NOT EXISTS `#P#category` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL DEFAULT '' COMMENT '分类名',
`icon_key` varchar(32) NOT NULL DEFAULT '' COMMENT '图标key',
`color` varchar(16) NOT NULL DEFAULT '' COMMENT '主题色',
`sort` int(10) NOT NULL DEFAULT '0' COMMENT '排序',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
`post_count` int(10) NOT NULL DEFAULT '0' COMMENT '信息量',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-分类';",
'banner' => "CREATE TABLE IF NOT EXISTS `#P#banner` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(64) NOT NULL DEFAULT '',
`image` varchar(255) NOT NULL DEFAULT '' COMMENT '图片,空则用渐变占位',
`link` varchar(255) NOT NULL DEFAULT '' COMMENT '跳转路径',
`color` varchar(255) NOT NULL DEFAULT '' COMMENT '底色:CSS 渐变 linear-gradient(...),留空则用内置春绿渐变',
`sort` int(10) NOT NULL DEFAULT '0',
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-轮播广告位';",
'area' => "CREATE TABLE IF NOT EXISTS `#P#area` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`province` varchar(32) NOT NULL DEFAULT '' COMMENT '省份',
`city` varchar(32) NOT NULL DEFAULT '' COMMENT '城市',
`district` varchar(32) NOT NULL DEFAULT '' COMMENT '区县(最终服务区域)',
`lat` decimal(10,6) DEFAULT NULL COMMENT '中心纬度(gcj02)',
`lng` decimal(10,6) DEFAULT NULL COMMENT '中心经度(gcj02)',
`sort` int(10) NOT NULL DEFAULT '0',
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `province_city` (`province`,`city`),
KEY `enabled` (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-服务区域(省/市/区县平铺,区县为服务区域)';",
'post' => "CREATE TABLE IF NOT EXISTS `#P#post` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '发布者',
`category_id` int(10) NOT NULL DEFAULT '0',
`title` varchar(80) NOT NULL DEFAULT '',
`content` text,
`cover` varchar(255) NOT NULL DEFAULT '',
`media` text COMMENT '媒体JSON数组',
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
`price_unit` varchar(16) NOT NULL DEFAULT '',
`area` varchar(32) NOT NULL DEFAULT '' COMMENT '区县名称(展示用)',
`district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)',
`address` varchar(128) NOT NULL DEFAULT '',
`contact` varchar(32) NOT NULL DEFAULT '' COMMENT '联系人',
`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系电话(完整)',
`wechat` varchar(64) NOT NULL DEFAULT '',
`tags` varchar(128) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
`is_paid` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否付费发布',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '支付金额',
`pay_type` varchar(16) NOT NULL DEFAULT '',
`audit_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待审1通过2驳回',
`audit_reason` varchar(255) NOT NULL DEFAULT '',
`views` int(10) NOT NULL DEFAULT '0',
`likes` int(10) NOT NULL DEFAULT '0',
`comment_count` int(10) NOT NULL DEFAULT '0',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常0下架',
`is_top` tinyint(1) NOT NULL DEFAULT '0',
`distance` int(10) NOT NULL DEFAULT '0' COMMENT '距离(米)',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
`deletetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `category_id` (`category_id`),
KEY `user_id` (`user_id`),
KEY `audit_status` (`audit_status`),
KEY `district_id` (`district_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-信息';",
'draft' => "CREATE TABLE IF NOT EXISTS `#P#draft` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`category_id` int(10) NOT NULL DEFAULT '0',
`title` varchar(80) NOT NULL DEFAULT '',
`content` text,
`media` text,
`contact` varchar(32) NOT NULL DEFAULT '',
`phone` varchar(20) NOT NULL DEFAULT '',
`address` varchar(128) NOT NULL DEFAULT '',
`area` varchar(32) NOT NULL DEFAULT '' COMMENT '区县名称',
`district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)',
`savedat` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-草稿箱';",
'merchant' => "CREATE TABLE IF NOT EXISTS `#P#merchant` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`name` varchar(64) NOT NULL DEFAULT '',
`logo` varchar(255) NOT NULL DEFAULT '',
`contact` varchar(32) NOT NULL DEFAULT '',
`category` varchar(32) NOT NULL DEFAULT '' COMMENT '经营类目',
`license` varchar(255) NOT NULL DEFAULT '' COMMENT '营业执照',
`audit_status` tinyint(1) NOT NULL DEFAULT '0',
`audit_reason` varchar(255) NOT NULL DEFAULT '',
`shop_status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'normal/pending/banned',
`violations` int(10) NOT NULL DEFAULT '0',
`post_count` int(10) NOT NULL DEFAULT '0',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-商家资质';",
'comment' => "CREATE TABLE IF NOT EXISTS `#P#comment` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`post_id` int(10) unsigned NOT NULL DEFAULT '0',
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`score` tinyint(1) NOT NULL DEFAULT '5',
`content` varchar(500) NOT NULL DEFAULT '',
`images` text,
`reply` varchar(500) NOT NULL DEFAULT '',
`replytime` int(10) DEFAULT NULL,
`top` tinyint(1) NOT NULL DEFAULT '0',
`status` tinyint(1) NOT NULL DEFAULT '1',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `post_id` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-评价';",
'report' => "CREATE TABLE IF NOT EXISTS `#P#report` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`target_type` varchar(16) NOT NULL DEFAULT 'post' COMMENT 'post/comment',
`target_id` int(10) NOT NULL DEFAULT '0',
`reason` varchar(255) NOT NULL DEFAULT '',
`reporter_id` int(10) unsigned NOT NULL DEFAULT '0',
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/handled',
`remark` varchar(255) NOT NULL DEFAULT '',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-举报';",
'order' => "CREATE TABLE IF NOT EXISTS `#P#order` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL DEFAULT '',
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`type` varchar(16) NOT NULL DEFAULT 'post' COMMENT 'post/member/recharge',
`title` varchar(64) NOT NULL DEFAULT '',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`pay_type` varchar(16) NOT NULL DEFAULT '',
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/refunding/refunded/closed',
`paytime` int(10) DEFAULT NULL,
`origin_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠前金额',
`coupon_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠券抵扣金额',
`coupon_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用的用户优惠券ID(fc_user_coupon.id)',
`refund_amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`refund_reason` varchar(255) NOT NULL DEFAULT '',
`out_refund_no` varchar(64) NOT NULL DEFAULT '' COMMENT '微信退款单号(商户侧)',
`related_id` int(10) NOT NULL DEFAULT '0' COMMENT '关联业务ID',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `order_no` (`order_no`),
KEY `user_id` (`user_id`),
KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-订单';",
'plan' => "CREATE TABLE IF NOT EXISTS `#P#plan` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`plan_code` varchar(16) NOT NULL DEFAULT '' COMMENT 'M1/M3/M12',
`name` varchar(32) NOT NULL DEFAULT '',
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
`origin_price` decimal(10,2) NOT NULL DEFAULT '0.00',
`days` int(10) NOT NULL DEFAULT '30',
`post_quota` int(10) NOT NULL DEFAULT '0' COMMENT '含发布条数(月/年套餐赠送额度)',
`tag` varchar(16) NOT NULL DEFAULT '',
`sort` int(10) NOT NULL DEFAULT '0',
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plan_code` (`plan_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-会员套餐';",
'user_ext' => "CREATE TABLE IF NOT EXISTS `#P#user_ext` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`openid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信openid',
`user_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1个人2商家',
`realname_status` tinyint(1) NOT NULL DEFAULT '0',
`realname` varchar(32) NOT NULL DEFAULT '' COMMENT '收款人真实姓名(提现用)',
`is_member` tinyint(1) NOT NULL DEFAULT '0',
`member_level` varchar(32) NOT NULL DEFAULT '',
`member_expiretime` int(10) DEFAULT NULL,
`invite_code` varchar(16) NOT NULL DEFAULT '',
`inviter_id` int(10) unsigned NOT NULL DEFAULT '0',
`free_view_date` varchar(10) NOT NULL DEFAULT '' COMMENT '免费查看额度基准日',
`free_view_used` int(10) NOT NULL DEFAULT '0',
`post_quota_left` int(10) NOT NULL DEFAULT '0' COMMENT '剩余发布条数(月/年套餐赠送)',
`post_quota_expire` int(10) DEFAULT NULL COMMENT '发布额度有效期(=会员到期时间)',
`pay_password` varchar(255) NOT NULL DEFAULT '' COMMENT '支付密码(password_hash,空=未设置)',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`),
KEY `invite_code` (`invite_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-用户扩展';",
'dist_relation' => "CREATE TABLE IF NOT EXISTS `#P#dist_relation` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '下线用户',
`parent_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上级用户',
`level` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1一级2二级',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id_level` (`user_id`,`level`),
KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-分销关系';",
'commission' => "CREATE TABLE IF NOT EXISTS `#P#commission` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '获得佣金的人',
`from_user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '产生佣金的下线',
`order_id` int(10) unsigned NOT NULL DEFAULT '0',
`order_no` varchar(32) NOT NULL DEFAULT '',
`order_type` varchar(16) NOT NULL DEFAULT '',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '佣金金额',
`rate` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '分佣比例%',
`level` tinyint(1) NOT NULL DEFAULT '1',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未结算1已结算',
`settle_due` int(10) DEFAULT NULL,
`settletime` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-分销佣金';",
'withdraw' => "CREATE TABLE IF NOT EXISTS `#P#withdraw` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`withdraw_no` varchar(32) NOT NULL DEFAULT '',
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`channel` varchar(16) NOT NULL DEFAULT 'wxpay',
`status` varchar(16) NOT NULL DEFAULT 'auditing' COMMENT 'auditing/paying/paid/failed/rejected',
`reason` varchar(255) NOT NULL DEFAULT '' COMMENT '驳回原因',
`paytime` int(10) DEFAULT NULL,
`out_bill_no` varchar(32) NOT NULL DEFAULT '' COMMENT '微信商家转账商户单号',
`transfer_status` varchar(16) NOT NULL DEFAULT '' COMMENT 'WAIT_USER_CONFIRM/PROCESSING/SUCCESS/FAIL/CANCELLED',
`transfer_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信转账单号',
`package_info` varchar(512) NOT NULL DEFAULT '' COMMENT '拉起确认收款页的package',
`fail_reason` varchar(255) NOT NULL DEFAULT '',
`transfer_time` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `withdraw_no` (`withdraw_no`),
KEY `user_id` (`user_id`),
KEY `out_bill_no` (`out_bill_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-提现';",
'finance_flow' => "CREATE TABLE IF NOT EXISTS `#P#finance_flow` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '归属用户,0=平台/历史数据',
`flow_no` varchar(32) NOT NULL DEFAULT '',
`type` varchar(16) NOT NULL DEFAULT 'income' COMMENT 'income/expense',
`scene` varchar(32) NOT NULL DEFAULT '' COMMENT '业务场景',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`balance` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '账后余额',
`related_id` int(10) NOT NULL DEFAULT '0',
`remark` varchar(255) NOT NULL DEFAULT '',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `flow_no` (`flow_no`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-资金流水';",
'message' => "CREATE TABLE IF NOT EXISTS `#P#message` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '0=全体',
`type` varchar(16) NOT NULL DEFAULT 'system' COMMENT 'system/interact',
`title` varchar(64) NOT NULL DEFAULT '',
`content` varchar(500) NOT NULL DEFAULT '',
`isread` tinyint(1) NOT NULL DEFAULT '0',
`readtime` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-消息';",
'pay_view' => "CREATE TABLE IF NOT EXISTS `#P#pay_view` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`post_id` int(10) unsigned NOT NULL DEFAULT '0',
`pay_type` varchar(16) NOT NULL DEFAULT '',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`order_id` int(10) unsigned NOT NULL DEFAULT '0',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_post` (`user_id`,`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-付费查看记录';",
'favorite' => "CREATE TABLE IF NOT EXISTS `#P#favorite` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '收藏人',
`post_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '信息ID',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_post` (`user_id`,`post_id`),
KEY `post_id` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-信息收藏';",
'follow' => "CREATE TABLE IF NOT EXISTS `#P#follow` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '关注发起人',
`target_user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被关注人',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_target` (`user_id`,`target_user_id`),
KEY `target_user_id` (`target_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-用户关注';",
'coupon' => "CREATE TABLE IF NOT EXISTS `#P#coupon` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '优惠券名称',
`type` varchar(16) NOT NULL DEFAULT 'fixed' COMMENT 'fixed 满减 / discount 折扣',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '面额(元);discount 时为折扣数如 8.50',
`min_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '使用门槛(元),0 为无门槛',
`total` int(10) NOT NULL DEFAULT '0' COMMENT '发行总量,0 为不限量',
`received` int(10) NOT NULL DEFAULT '0' COMMENT '已领取数量',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:1 启用 0 停用',
`sort` int(10) NOT NULL DEFAULT '0' COMMENT '排序,越大越靠前',
`days` int(10) NOT NULL DEFAULT '0' COMMENT '领取后有效天数,0 为永久',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `enabled` (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-优惠券';",
'user_coupon' => "CREATE TABLE IF NOT EXISTS `#P#user_coupon` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '领取人',
`coupon_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '优惠券ID',
`status` varchar(16) NOT NULL DEFAULT 'unused' COMMENT 'unused 未使用 / used 已使用 / expired 已过期',
`order_no` varchar(32) NOT NULL DEFAULT '' COMMENT '使用时的订单号',
`usetime` int(10) DEFAULT NULL COMMENT '使用时间',
`expiretime` int(10) DEFAULT NULL COMMENT '过期时间(领取时间 + days)',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `coupon_id` (`coupon_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-用户优惠券';",
];
}
/**
* 注入种子数据
* 说明:与小程序端 common/mock/data.js 的数据集逐字段对齐,
* 保证「前端离线 Mock」与「线上真实接口」返回同一套数据,便于两端对拍。
*/
protected function seed(Output $output)
{
$now = time();
$day = 86400;
$t = function ($offsetDays) use ($now, $day) {
return (int)($now - $offsetDays * $day);
};
/* ---------- 业务配置 ---------- */
$configs = [
['base.appName', '同城分类信息', 'base', '小程序名称'],
['base.logo', '', 'base', 'LOGO地址'],
['base.servicePhone', '400-000-0000', 'base', '客服电话'],
['base.aboutUs', '本地同城分类信息服务平台,招聘/房产/二手/服务一站直达。', 'base', '关于我们'],
['base.shareTitle', '同城分类信息 · 身边的好帮手', 'base', '分享标题'],
['wx.appid', 'wxbfcb778f3019cff8', 'wx', '小程序AppID(jscode2session 用)'],
['wx.secret', 'b023726713eebe3dad3876f5cea8ee6a', 'wx', '小程序AppSecret(仅服务端持有,不下发前端)'],
['pay.mchId', '', 'pay', '微信支付商户号'],
['pay.apiKey', '', 'pay', '微信支付密钥'],
// 发布模式与发布价格(后台「系统配置 → 发布配置」维护)
// 套餐价格与含发布条数在 fc_plan(后台「运营推广 → 会员套餐」维护),不走配置键
['post.mode', 'free', 'post', '发布模式:free 免费发布 / paid 付费发布(默认免费,避免上线即撞付费墙)'],
['post.singlePrice', '5', 'post', '单条发布价格(元)=付费发布标准价'],
['post.freeQuota', '0', 'post', '免费发布条数(0 = 不限制/免费到底;达到条数后按单条发布价格计费)'],
['post.freeQuotaCycle', 'day', 'post', '免费条数统计周期:day 每日 / month 每月 / total 累计'],
['dist.enabled', '1', 'dist', '是否开启分销'],
['dist.level', '2', 'dist', '分佣层级'],
['dist.level1Rate', '20', 'dist', '一级分佣比例%'],
['dist.level2Rate', '10', 'dist', '二级分佣比例%'],
['dist.settleCycle', 'T+7', 'dist', '结算周期'],
['dist.minWithdraw', '50', 'dist', '最低提现金额'],
['member.benefit', json_encode([
'freeView' => 20,
'postDiscount' => '8 折',
'distRate' => '一级 20% / 二级 10%',
'benefits' => ['每日免费查看 20 条联系方式', '发布信息 8 折', '专属会员标识', '赠送大量发布额度'],
], JSON_UNESCAPED_UNICODE), 'member', '会员权益配置'],
['ui.siteName', '同城分类信息', 'ui', '站点名称'],
['ui.logo', '', 'ui', '自定义LOGO'],
['ui.icons', '{}', 'ui', '图标覆盖 {key:url}'],
['ui.updatedAt', date('c'), 'ui', '最后更新时间'],
];
foreach ($configs as $c) {
Db::name('fc_config')->insert([
'key' => $c[0], 'value' => $c[1], 'group_key' => $c[2], 'remark' => $c[3],
'createtime' => $now, 'updatetime' => $now,
]);
}
$output->writeln(' - 配置 ' . count($configs) . ' 项');
/* ---------- 分类 ---------- */
$categories = [
[1, '招聘求职', 'cat-job', '#FF7A59', 1280],
[2, '房产租售', 'cat-house', '#FF9F45', 860],
[3, '二手闲置', 'cat-goods', '#FFB570', 2340],
[4, '车辆买卖', 'cat-car', '#F2994A', 430],
[5, '本地服务', 'cat-service', '#E8845F', 760],
[6, '教育培训', 'cat-education', '#F4A261', 320],
[7, '家政保洁', 'cat-clean', '#FFA07A', 210],
[8, '同城交友', 'cat-friend', '#FF8C69', 190],
];
foreach ($categories as $i => $c) {
Db::name('fc_category')->insert([
'id' => $c[0], 'name' => $c[1], 'icon_key' => $c[2], 'color' => $c[3],
'sort' => $i + 1, 'enabled' => 1, 'post_count' => $c[4],
'createtime' => $now, 'updatetime' => $now,
]);
}
/* ---------- 轮播 ---------- */
$banners = self::BANNERS;
foreach ($banners as $i => $b) {
Db::name('fc_banner')->insert([
'title' => $b[0], 'image' => '/uploads/banners/banner-' . ($i + 1) . '.png', 'link' => $b[1], 'color' => $b[2],
'sort' => $i + 1, 'enabled' => 1, 'createtime' => $now, 'updatetime' => $now,
]);
}
/* ---------- 服务区域(省 / 市 / 区县平铺,区县为服务区域) ---------- */
$areas = [
['贵州省', '贵阳市', '云岩区', 1, 1, 26.605, 106.710],
['贵州省', '贵阳市', '南明区', 2, 1, 26.568, 106.716],
['贵州省', '贵阳市', '观山湖区', 3, 1, 26.625, 106.625],
['贵州省', '贵阳市', '花溪区', 4, 1, 26.430, 106.670],
['贵州省', '贵阳市', '白云区', 5, 0, 26.678, 106.623],
];
foreach ($areas as $a) {
Db::name('fc_area')->insert([
'province' => $a[0], 'city' => $a[1], 'district' => $a[2],
'lat' => $a[5], 'lng' => $a[6],
'sort' => $a[3], 'enabled' => $a[4],
'createtime' => $now, 'updatetime' => $now,
]);
}
/* ---------- 会员套餐 ---------- */
$plans = [
['M1', '月卡', 19.9, 29.9, 30, '尝鲜', 30],
['M3', '季卡', 49.9, 89.7, 90, '最划算', 90],
['M12', '年卡', 168, 358.8, 365, '省心', 500],
];
foreach ($plans as $i => $p) {
Db::name('fc_plan')->insert([
'plan_code' => $p[0], 'name' => $p[1], 'price' => $p[2], 'origin_price' => $p[3],
'days' => $p[4], 'tag' => $p[5], 'sort' => $i + 1, 'enabled' => 1,
'post_quota' => $p[6],
'createtime' => $now, 'updatetime' => $now,
]);
}
$output->writeln(' - 分类8 / 轮播3 / 服务区域5 / 套餐3');
/* ---------- 演示用户(显式ID,与前端 mock 数据集一致) ---------- */
$salt = 'fcseed';
$pwd = md5(md5('123456') . $salt);
$users = [
[1001, '同城小助手', '13800001111', 128.50, 320],
[1002, '小王同学', '15033443344', 12.00, 40],
[1003, '洁净到家', '13156785678', 60.00, 120],
[1004, '违规用户A', '13000000000', 0.00, 0],
[2001, '顺捷物流', '13988888888', 300.00, 210],
[2002, '安家房产', '13566666666', 520.00, 380],
[2003, '骑行爱好者', '13712341234', 8.00, 15],
[2004, '车行天下', '13800000000', 90.00, 66],
[2005, '洁净到家', '13156785678', 45.00, 88],
[2006, '小王同学', '15033443344', 6.00, 10],
[2007, '优选二手车', '13700000000', 0.00, 0],
];
// 幂等:先清理同 ID 的演示用户(fa_user 属核心表,不在 truncateAll 范围内)
$demoUserIds = array_column($users, 0);
Db::name('user')->where('id', 'in', $demoUserIds)->delete();
Db::name('fc_user_ext')->where('user_id', 'in', $demoUserIds)->delete();
foreach ($users as $u) {
Db::name('user')->insert([
'id' => $u[0], 'group_id' => 1, 'username' => 'fc' . $u[0], 'nickname' => $u[1],
'password' => $pwd, 'salt' => $salt, 'email' => $u[0] . '@fenleixinxi.local',
'mobile' => $u[2], 'avatar' => '', 'level' => 1, 'gender' => 0,
'money' => $u[3], 'score' => $u[4], 'status' => ($u[0] === 1004 ? 'hidden' : 'normal'),
'jointime' => $t(180), 'joinip' => '127.0.0.1', 'logintime' => $t(0.1),
'loginip' => '127.0.0.1', 'createtime' => $t(180), 'updatetime' => $t(0.1),
]);
}
/* 用户扩展(会员状态 / 身份 / 邀请码) */
$exts = [
[1001, 1, 1, '年卡会员', 300, 'TC8A9K', 0, 1],
[1002, 1, 0, '', 0, 'TC2B7M', 1001, 1],
[1003, 2, 1, '月卡会员', 25, 'TC3C1P', 1001, 2],
[1004, 1, 0, '', 0, 'TC4D5Q', 0, 0],
[2001, 2, 1, '季卡会员', 60, 'TC5E8R', 0, 2],
[2002, 2, 1, '年卡会员', 200, 'TC6F2S', 0, 2],
[2003, 1, 0, '', 0, 'TC7G9T', 1001, 1],
[2004, 2, 0, '', 0, 'TC8H3U', 1002, 2],
[2005, 2, 1, '月卡会员', 20, 'TC9J6V', 1001, 2],
[2006, 1, 0, '', 0, 'TC2K4W', 1002, 1],
[2007, 2, 0, '', 0, 'TC3L7X', 0, 2],
];
foreach ($exts as $e) {
Db::name('fc_user_ext')->insert([
'user_id' => $e[0], 'openid' => 'openid_' . $e[0], 'user_type' => $e[1],
'realname_status' => 1, 'is_member' => $e[2], 'member_level' => $e[3],
'member_expiretime' => $e[4] > 0 ? $t(-$e[4]) : null,
'invite_code' => $e[5], 'inviter_id' => $e[6],
'free_view_date' => date('Y-m-d'), 'free_view_used' => 0,
'createtime' => $t(180), 'updatetime' => $t(0.1),
]);
}
$output->writeln(' - 用户 ' . count($users) . ' 个 + 扩展');
/* ---------- 信息(覆盖待审/已通过/已驳回三种状态) ---------- */
$posts = [
[101, 2001, 1, '急招快递分拣员(日结)', '站点直招,日结 180-220 元,包工作餐,当日结算。有意者电话联系。',
0, '面议', '云岩区', '贵阳北站物流园', 1892, 32, 0, 1, 12, 1200, '日结,包吃', 0],
[102, 2002, 2, '整租·未来方舟精装一室一厅', '家电齐全,拎包入住,近地铁 2 号线,押一付一,可短租。',
1800, '元/月', '南明区', '未来方舟 B 区', 3204, 88, 1, 1, 26, 3400, '精装,近地铁', 0],
[103, 2003, 3, '九成新山地自行车 转让', '骑行不到 300 公里,27 速,原价 1599,现 680 出,可小刀,同城自提。',
680, '元', '观山湖区', '世纪城', 640, 21, 0, 1, 4, 820, '自提,9成新', 0],
[104, 2004, 4, '2019 款家用轿车 一手车', '车况精品,无事故无泡水,4S 店保养记录齐全,支持第三方检测。',
68000, '元', '云岩区', '贵阳市云岩区', 1120, 40, 1, 0, 0, 5100, '无事故,可检测', 0],
[105, 2005, 5, '专业空调清洗 / 加氟上门', '10 年老师傅,上门服务,清洗 60 元/台,加氟 80 元起,不满意不收费。',
60, '元/台', '南明区', '花果园', 2310, 66, 0, 0, 18, 2600, '上门,不满意不收费', 0],
[106, 2006, 3, 'iPad 2021 64G 深空灰', '学生自用,无磕碰,配件齐全,支持当面验机。',
1450, '元', '观山湖区', '贵阳奥体中心', 480, 15, 0, 2, 1, 1500, '验机', 0],
];
$offsets = [0.2, 0.5, 1, 0.1, 0.05, 2];
$phones = [101 => '13988888888', 102 => '13566666666', 103 => '13712341234',
104 => '13800000000', 105 => '13156785678', 106 => '15033443344'];
$paids = [101 => 0, 102 => 1, 103 => 0, 104 => 1, 105 => 0, 106 => 0];
foreach ($posts as $i => $p) {
$id = $p[0];
Db::name('fc_post')->insert([
'id' => $id, 'user_id' => $p[1], 'category_id' => $p[2], 'title' => $p[3], 'content' => $p[4],
'cover' => '', 'media' => json_encode([], JSON_UNESCAPED_UNICODE),
'price' => $p[5], 'price_unit' => $p[6], 'area' => $p[7], 'address' => $p[8],
'contact' => '联系人', 'phone' => $phones[$id], 'wechat' => 'tongcheng_' . $id,
'tags' => $p[15],
'is_paid' => $paids[$id], 'amount' => $paids[$id] ? 5 : 0, 'pay_type' => $paids[$id] ? 'wxpay' : '',
'audit_status' => $p[12], 'audit_reason' => $p[12] == 2 ? '信息涉嫌重复发布' : '',
'views' => $p[9], 'likes' => $p[10], 'comment_count' => $p[13],
'status' => 1, 'is_top' => 0, 'distance' => $p[14],
'createtime' => $t($offsets[$i]), 'updatetime' => $t($offsets[$i]),
]);
}
$output->writeln(' - 信息 ' . count($posts) . ' 条');
/* ---------- 草稿 ---------- */
Db::name('fc_draft')->insert([
'user_id' => 1001, 'category_id' => 2, 'title' => '转租单间(草稿)', 'content' => '还未填完……',
'media' => json_encode([], JSON_UNESCAPED_UNICODE), 'savedat' => $t(0.1),
'createtime' => $t(0.1), 'updatetime' => $t(0.1),
]);
/* ---------- 商家资质 ---------- */
$merchants = [
[2001, '顺捷物流', '13988888888', '物流服务', 1, 'normal', 0, 32, 120],
[2002, '安家房产', '13566666666', '房产中介', 1, 'normal', 1, 68, 200],
[2005, '洁净到家', '13156785678', '家政服务', 0, 'pending', 0, 12, 3],
[2007, '优选二手车', '13700000000', '汽车服务', 2, 'banned', 3, 0, 60],
];
foreach ($merchants as $m) {
Db::name('fc_merchant')->insert([
'user_id' => $m[0], 'name' => $m[1], 'logo' => '', 'contact' => $m[2], 'category' => $m[3],
'license' => $m[4] == 2 ? '' : '营业执照.jpg',
'audit_status' => $m[4], 'audit_reason' => $m[4] == 2 ? '营业执照不清晰' : '',
'shop_status' => $m[5], 'violations' => $m[6], 'post_count' => $m[7],
'createtime' => $t($m[8]), 'updatetime' => $t($m[8]),
]);
}
/* ---------- 评价 ---------- */
$comments = [
[5001, 101, 1002, 5, '联系很及时,当天就上岗了。', '', 0, 0.5],
[5002, 102, 1003, 4, '房子不错,房东好说话。', '感谢支持,欢迎续租~', 1, 2],
[5003, 103, 1004, 1, '广告信息,疑似虚假。', '', 0, 0.2],
];
foreach ($comments as $c) {
Db::name('fc_comment')->insert([
'id' => $c[0], 'post_id' => $c[1], 'user_id' => $c[2], 'score' => $c[3], 'content' => $c[4],
'images' => json_encode([], JSON_UNESCAPED_UNICODE), 'reply' => $c[5],
'replytime' => $c[5] ? $t($c[7]) : null, 'top' => $c[6], 'status' => 1,
'createtime' => $t($c[7]), 'updatetime' => $t($c[7]),
]);
}
/* ---------- 举报 ---------- */
Db::name('fc_report')->insert([
'target_type' => 'comment', 'target_id' => 5003, 'reason' => '疑似虚假信息',
'reporter_id' => 1002, 'status' => 'pending', 'remark' => '',
'createtime' => $t(0.2), 'updatetime' => $t(0.2),
]);
Db::name('fc_report')->insert([
'target_type' => 'post', 'target_id' => 106, 'reason' => '重复发布',
'reporter_id' => 1004, 'status' => 'handled', 'remark' => '已下架处理',
'createtime' => $t(4), 'updatetime' => $t(4),
]);
$output->writeln(' - 商家4 / 评价3 / 举报2');
/* ---------- 订单(发布 / 会员 / 充值 三类) ---------- */
$orders = [
[3001, 'PO20240101001', 1001, 'post', '单条付费发布', 5, 'wxpay', 'paid', 1],
[3002, 'MO20240102002', 1001, 'member', '年卡会员', 168, 'wxpay', 'paid', 2],
[3003, 'RO20240103003', 1001, 'recharge', '余额充值', 100, 'wxpay', 'paid', 3],
[3004, 'PO20240104004', 1001, 'post', '单条付费发布', 5, 'balance', 'pending', 0.5],
];
foreach ($orders as $o) {
Db::name('fc_order')->insert([
'id' => $o[0], 'order_no' => $o[1], 'user_id' => $o[2], 'type' => $o[3], 'title' => $o[4],
'amount' => $o[5], 'pay_type' => $o[6], 'status' => $o[7],
'paytime' => $o[7] === 'paid' ? $t($o[8]) : null,
'related_id' => 0, 'createtime' => $t($o[8]), 'updatetime' => $t($o[8]),
]);
}
/* ---------- 消息中心 ---------- */
$messages = [
[1001, 'system', '审核通过', '您发布的信息「急招快递分拣员(日结)」已审核通过。', 0, 0.2],
[1001, 'system', '会员到期提醒', '您的年卡会员将于 30 天后到期,续费可享 9 折。', 0, 1],
[1001, 'interact', '收到新评价', '用户「小王同学」评价了您的信息:服务很到位,五星好评!', 1, 2],
[1001, 'interact', '点赞提醒', '您发布的信息获得 5 个赞。', 1, 3],
];
foreach ($messages as $m) {
Db::name('fc_message')->insert([
'user_id' => $m[0], 'type' => $m[1], 'title' => $m[2], 'content' => $m[3],
'isread' => $m[4], 'readtime' => $m[4] ? $t($m[5]) : null,
'createtime' => $t($m[5]),
]);
}
/* ---------- 分销关系(1001 的一/二级下线) ---------- */
$rels = [[1001, 1], [1002, 1], [2003, 1], [1003, 2], [2004, 2], [2005, 2], [2006, 2]];
foreach ($rels as $r) {
Db::name('fc_dist_relation')->insert([
'user_id' => $r[0], 'parent_id' => 1001, 'level' => $r[1], 'createtime' => $t(10),
]);
}
/* ---------- 佣金(1001 的收益明细) ----------
* 数值与小程序 mock 的 distOverview 对齐:
* totalCommission 1286.4 = settled 336.4 + unsettled 950
* 已申请提现 150(auditing 50 + paid 100)
* → withdrawable = 336.4 - 150 = 186.4
*/
$commissions = [
[1002, 3002, 'MO20240102002', 'member', 900.0, 20, 1, 0, 2],
[2003, 3001, 'PO20240101001', 'post', 50.0, 20, 1, 0, 0],
[2004, 3003, 'RO20240103003', 'recharge', 1.0, 20, 1, 1, 3],
[1003, 3002, 'MO20240102002', 'member', 300.0, 10, 2, 1, 5],
[2005, 3003, 'RO20240103003', 'recharge', 35.4, 10, 2, 1, 6],
];
foreach ($commissions as $c) {
Db::name('fc_commission')->insert([
'user_id' => 1001,
'from_user_id' => $c[0],
'order_id' => $c[1],
'order_no' => $c[2],
'order_type' => $c[3],
'amount' => $c[4],
'rate' => $c[5],
'level' => $c[6],
'status' => $c[7],
'settletime' => $c[7] ? $t($c[8]) : null,
'createtime' => $t($c[8]),
]);
}
/* ---------- 提现(含待审/已打款) ---------- */
$withdraws = [
[1, 'WD20231220001', 1001, 100, 'paid', '', 25],
[2, 'WD20240105002', 1001, 50, 'auditing', '', 1],
[3, 'WD20240106003', 1002, 200, 'auditing', '', 0.4],
];
foreach ($withdraws as $w) {
Db::name('fc_withdraw')->insert([
'id' => $w[0], 'withdraw_no' => $w[1], 'user_id' => $w[2], 'amount' => $w[3],
'channel' => 'wxpay', 'status' => $w[4], 'reason' => $w[5],
'paytime' => $w[4] === 'paid' ? $t($w[6]) : null,
'createtime' => $t($w[6]), 'updatetime' => $t($w[6]),
]);
}
/* ---------- 资金流水 ---------- */
$flows = [
['F20240107001', 'income', '发布付费', 5, 2864.5, 0.2],
['F20240107002', 'income', '会员开通', 168, 2859.5, 0.5],
['F20240106003', 'expense', '佣金提现', 100, 2691.5, 1],
['F20240105004', 'income', '余额充值', 100, 2791.5, 2],
];
foreach ($flows as $f) {
Db::name('fc_finance_flow')->insert([
'flow_no' => $f[0], 'type' => $f[1], 'scene' => $f[2], 'amount' => $f[3],
'balance' => $f[4], 'related_id' => 0, 'remark' => '',
'createtime' => $t($f[5]),
]);
}
$output->writeln(' - 订单4 / 消息4 / 分销7 / 佣金2 / 提现3 / 流水4');
}
}