diff --git a/application/admin/command/FcInstall.php b/application/admin/command/FcInstall.php
index 03a51de..d520a57 100644
--- a/application/admin/command/FcInstall.php
+++ b/application/admin/command/FcInstall.php
@@ -27,9 +27,9 @@ class FcInstall extends Command
* image 留空时小程序用「亮绿底 + 墨绿字 + 白色插画」内置配图渲染。
*/
const BANNERS = [
- ['同城好物节 · 二手直降', '/pages/post/list?categoryId=3', 'linear-gradient(105deg, #00e070 0%, #00c860 42%, #046b3b 100%)'],
- ['招聘旺季 · 免费发布', '/pages/post/create', 'linear-gradient(105deg, #00d96c 0%, #00b85c 40%, #023a21 100%)'],
- ['本地服务 · 上门更省心', '/pages/post/list?categoryId=5', 'linear-gradient(105deg, #00e878 0%, #049e52 45%, #045c34 100%)'],
+ ['同城好物节 · 二手直降', '/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%)'],
];
/**
diff --git a/application/admin/command/FcSeedPosts.php b/application/admin/command/FcSeedPosts.php
index 7e4ea68..3dbe45f 100644
--- a/application/admin/command/FcSeedPosts.php
+++ b/application/admin/command/FcSeedPosts.php
@@ -24,14 +24,14 @@ class FcSeedPosts extends Command
const USERS = [
1001 => ['同城小助手', '13800001111'],
1002 => ['小王同学', '15033443344'],
- 1003 => ['洁净到家', '13156785678'],
- 2001 => ['顺捷物流', '13988888888'],
- 2002 => ['安家房产', '13566666666'],
- 2003 => ['骑行爱好者', '13712341234'],
- 2004 => ['车行天下', '13800000000'],
- 2005 => ['洁净到家', '13156785678'],
- 2006 => ['小王同学', '15033443344'],
- 2007 => ['优选二手车', '13700000000'],
+ 1003 => ['洁净到家·陈姐', '13156785678'],
+ 2001 => ['顺捷物流·杨经理', '13988888888'],
+ 2002 => ['安家房产·周主管', '13566666666'],
+ 2003 => ['骑行爱好者·小周', '13712341234'],
+ 2004 => ['车行天下·张总', '13800000000'],
+ 2005 => ['洁净到家·王师傅', '13156785678'],
+ 2006 => ['数码达人·阿豪', '15033443344'],
+ 2007 => ['优选二手车·赵经理', '13700000000'],
];
/** 区域中心坐标(gcj02 近似值,用于首页「附近」排序演示) */
@@ -82,6 +82,7 @@ class FcSeedPosts extends Command
'id' => $id,
'user_id' => $uid,
'category_id' => $categoryId,
+ 'identity_type' => in_array($uid, [1003, 2001, 2002, 2004, 2005, 2007, 2011], true) ? 2 : 1,
'title' => $row[1],
'content' => $row[2],
'cover' => $media[0],
@@ -460,5 +461,17 @@ class FcSeedPosts extends Command
->count();
Db::name('fc_category')->where('id', $categoryId)->update(['post_count' => $count]);
}
+
+ // 同步商家有效发帖数
+ $merchants = Db::name('fc_merchant')->field('id,user_id')->select();
+ foreach ($merchants as $m) {
+ $mCount = Db::name('fc_post')
+ ->where('user_id', $m['user_id'])
+ ->where('audit_status', 1)
+ ->where('status', 1)
+ ->whereNull('deletetime')
+ ->count();
+ Db::name('fc_merchant')->where('id', $m['id'])->update(['post_count' => $mCount]);
+ }
}
}
diff --git a/application/admin/model/FcMessage.php b/application/admin/model/FcMessage.php
index 987c86b..e0da291 100644
--- a/application/admin/model/FcMessage.php
+++ b/application/admin/model/FcMessage.php
@@ -3,44 +3,45 @@
namespace app\admin\model;
use think\Model;
+use traits\model\SoftDelete;
use app\common\library\FcRelation;
-
+/**
+ * 消息模型 · 同城分类信息
+ */
class FcMessage extends Model
{
-
-
-
-
+ use SoftDelete;
// 表名
protected $name = 'fc_message';
-
+
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
- protected $updateTime = false;
- protected $deleteTime = false;
+ protected $updateTime = 'updatetime';
+ protected $deleteTime = 'deletetime';
// 追加属性
protected $append = [
'user_name',
];
+ /**
+ * 获取接收用户名属性
+ *
+ * @param mixed $value 原值
+ * @param array $data 当前行数据
+ * @return string 用户昵称或标识
+ */
public function getUserNameAttr($value, $data)
{
- return FcRelation::userName(isset($data['user_id']) ? $data['user_id'] : 0);
+ $uid = isset($data['user_id']) ? (int)$data['user_id'] : 0;
+ if ($uid === 0) {
+ return '全体用户(广播)';
+ }
+ return FcRelation::userName($uid);
}
-
-
-
-
-
-
-
-
-
-
}
diff --git a/application/admin/view/fc/message/index.html b/application/admin/view/fc/message/index.html
index 96eef66..aab9001 100644
--- a/application/admin/view/fc/message/index.html
+++ b/application/admin/view/fc/message/index.html
@@ -6,15 +6,11 @@
where(function ($w) {
+ $w->where('amount', 'null')->whereOr('amount', '<=', 0)->whereOr('is_paid', 1);
+ });
+ };
+ $applyPaid($query);
+
$counter = Db::name('fc_post')
->where('audit_status', 1)
->where('status', 1)
@@ -233,6 +241,7 @@ class Home extends V1
$counter->where('title', 'like', '%' . $keyword . '%');
}
$applyArea($counter);
+ $applyPaid($counter);
$total = $counter->count();
// 「附近」:距离在 PHP 内用 Haversine 计算后排序(不用 SQL 距离函数,避免 MySQL 兼容问题);
diff --git a/application/api/controller/Member.php b/application/api/controller/Member.php
index 627efec..e057070 100644
--- a/application/api/controller/Member.php
+++ b/application/api/controller/Member.php
@@ -68,11 +68,40 @@ class Member extends V1
? array_values($cfg['benefits'])
: $default['benefits'];
+ $freeViewQuota = isset($cfg['freeView']) ? max(0, (int)$cfg['freeView']) : $default['freeView'];
+ $postDiscount = isset($cfg['postDiscount']) ? (string)$cfg['postDiscount'] : $default['postDiscount'];
+
+ // 真实查询当前用户的会员权益消耗与剩余额度
+ $me = $this->user();
+ $isMember = false;
+ $freeViewUsed = 0;
+ $freeViewLeft = 0;
+ $postQuotaLeft = 0;
+ $memberExpireAt = null;
+
+ if ($me) {
+ $now = time();
+ $ext = $this->ext($me['id']);
+ $isMember = (int)$ext['is_member'] === 1
+ && (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now);
+ $today = date('Y-m-d');
+ $freeViewUsed = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
+ $freeViewLeft = $isMember ? max(0, $freeViewQuota - $freeViewUsed) : 0;
+ $postQuotaLeft = $this->quotaLeft($ext);
+ $memberExpireAt = !empty($ext['member_expiretime']) ? $this->iso($ext['member_expiretime']) : null;
+ }
+
$this->ok([
- 'freeView' => isset($cfg['freeView']) ? (int)$cfg['freeView'] : $default['freeView'],
- 'postDiscount' => isset($cfg['postDiscount']) ? (string)$cfg['postDiscount'] : $default['postDiscount'],
- 'distRate' => isset($cfg['distRate']) ? (string)$cfg['distRate'] : $default['distRate'],
- 'benefits' => $benefits,
+ 'freeView' => $freeViewQuota,
+ 'postDiscount' => $postDiscount,
+ 'distRate' => isset($cfg['distRate']) ? (string)$cfg['distRate'] : $default['distRate'],
+ 'benefits' => $benefits,
+ 'isMember' => $isMember,
+ 'freeViewQuota' => $freeViewQuota,
+ 'freeViewUsed' => $freeViewUsed,
+ 'freeViewLeft' => $freeViewLeft,
+ 'postQuotaLeft' => $postQuotaLeft,
+ 'memberExpireAt' => $memberExpireAt,
]);
}
diff --git a/application/api/controller/Message.php b/application/api/controller/Message.php
index 1ff1e1a..edbb0b5 100644
--- a/application/api/controller/Message.php
+++ b/application/api/controller/Message.php
@@ -7,11 +7,16 @@ use think\Db;
/**
* 用户端 · 消息中心(Module-MESSAGE)
* 路由:
- * GET /api/v1/message/list M1 消息列表
- * POST /api/v1/message/:id/read M2 标记已读
- * GET /api/v1/message/unread-count M3 未读消息数(首页角标)
+ * GET /api/v1/message/list M1 消息列表(支持个人消息与系统广播)
+ * POST /api/v1/message/:id/read M2 标记单条已读
+ * GET /api/v1/message/unread-count M3 未读消息数(首页角标统计)
+ * POST /api/v1/message/read-all M4 一键已读(批量标记)
+ * POST /api/v1/message/clear M5 一键清理(批量软删除/屏蔽)
+ * POST /api/v1/message/:id/delete M6 删除单条消息(软删除/屏蔽)
*
- * 说明:user_id=0 表示全体广播消息,用户可见。
+ * 说明:
+ * - user_id > 0 为用户专属通知,支持物理或软删除(deletetime)。
+ * - user_id = 0 为全站系统广播,利用 read_uids 和 deleted_uids 支持多用户独立已读与独立清理,互不影响。
*/
class Message extends V1
{
@@ -19,6 +24,8 @@ class Message extends V1
* M1 消息列表
* GET /api/v1/message/list 入参 {type?,page,size}
* → 分页 {list:[{id,type,title,content,read,createdAt}],page,size,total,hasMore}
+ *
+ * @return void
*/
public function list()
{
@@ -28,8 +35,17 @@ class Message extends V1
// 闭包重建查询:TP5.0 的 count() 会清空 Query::options,不能复用同一对象
list($rows, $total, $page, $size) = $this->pageQuery(function ($forCount) use ($uid, $type) {
- $q = Db::name('fc_message')->where('user_id', 'in', [$uid, 0]);
- if ($type !== '' && $type !== null) {
+ $q = Db::name('fc_message')
+ ->whereNull('deletetime')
+ ->where(function ($sub) use ($uid) {
+ $sub->where('user_id', $uid)
+ ->whereOr(function ($sub2) use ($uid) {
+ $sub2->where('user_id', 0)
+ ->whereRaw("NOT FIND_IN_SET('{$uid}', IFNULL(deleted_uids, ''))");
+ });
+ });
+
+ if ($type !== '' && $type !== null && $type !== 'all') {
$q->where('type', $type);
}
if ($forCount) {
@@ -40,12 +56,21 @@ class Message extends V1
$list = [];
foreach ($rows as $row) {
+ // 计算当前用户下的已读状态(广播消息匹配 read_uids 集合)
+ $isRead = false;
+ if ((int)$row['user_id'] === $uid) {
+ $isRead = (bool)(int)$row['isread'];
+ } else {
+ $readUids = array_filter(explode(',', (string)$row['read_uids']));
+ $isRead = (bool)(int)$row['isread'] || in_array((string)$uid, $readUids);
+ }
+
$list[] = [
'id' => (int)$row['id'],
'type' => (string)$row['type'],
'title' => (string)$row['title'],
'content' => (string)$row['content'],
- 'read' => (bool)(int)$row['isread'],
+ 'read' => $isRead,
'createdAt' => $this->iso($row['createtime']),
];
}
@@ -54,8 +79,10 @@ class Message extends V1
}
/**
- * M2 标记已读
+ * M2 标记单条已读
* POST /api/v1/message/:id/read → {read:true}
+ *
+ * @return void
*/
public function read()
{
@@ -63,9 +90,31 @@ class Message extends V1
$uid = (int)$user['id'];
$id = (int)$this->request->param('id', 0);
- Db::name('fc_message')->where('id', $id)
- ->where('user_id', 'in', [$uid, 0])
- ->update(['isread' => 1, 'readtime' => time()]);
+ $msg = Db::name('fc_message')->where('id', $id)->whereNull('deletetime')->find();
+ if (!$msg) {
+ $this->fail(404, '消息不存在或已删除');
+ }
+
+ $now = time();
+ if ((int)$msg['user_id'] === $uid) {
+ Db::name('fc_message')->where('id', $id)->update([
+ 'isread' => 1,
+ 'readtime' => $now,
+ 'updatetime' => $now,
+ ]);
+ } elseif ((int)$msg['user_id'] === 0) {
+ // 系统广播:维护用户级已读集合 read_uids
+ $readUids = array_filter(explode(',', (string)$msg['read_uids']));
+ if (!in_array((string)$uid, $readUids)) {
+ $readUids[] = (string)$uid;
+ Db::name('fc_message')->where('id', $id)->update([
+ 'read_uids' => implode(',', array_unique($readUids)),
+ 'updatetime' => $now,
+ ]);
+ }
+ } else {
+ $this->fail(403, '无权操作此消息');
+ }
$this->ok(['read' => true]);
}
@@ -73,19 +122,184 @@ class Message extends V1
/**
* M3 未读消息数(首页消息角标)
* GET /api/v1/message/unread-count → {count}
- * 规则:统计当前用户可见(user_id = 本人 或 0 广播)且 isread=0 的消息条数,
- * 替代前端原先写死的角标数字。
+ * 规则:统计个人专属未读数 + 全体广播中未被当前用户读取的有效条数
+ *
+ * @return void
*/
public function unreadCount()
{
$user = $this->auth();
$uid = (int)$user['id'];
- $count = (int)Db::name('fc_message')
- ->where('user_id', 'in', [$uid, 0])
+ // 1. 专属个人消息未读数
+ $userCount = (int)Db::name('fc_message')
+ ->where('user_id', $uid)
->where('isread', 0)
+ ->whereNull('deletetime')
->count();
- $this->ok(['count' => $count]);
+ // 2. 系统广播未读数
+ $broadcastMsgs = Db::name('fc_message')
+ ->where('user_id', 0)
+ ->whereNull('deletetime')
+ ->select();
+
+ $broadcastCount = 0;
+ foreach ($broadcastMsgs as $b) {
+ $deletedUids = array_filter(explode(',', (string)$b['deleted_uids']));
+ if (in_array((string)$uid, $deletedUids)) {
+ continue; // 用户已删除该广播
+ }
+ if ((int)$b['isread'] === 1) {
+ continue; // 广播全局标记已读
+ }
+ $readUids = array_filter(explode(',', (string)$b['read_uids']));
+ if (!in_array((string)$uid, $readUids)) {
+ $broadcastCount++;
+ }
+ }
+
+ $this->ok(['count' => $userCount + $broadcastCount]);
+ }
+
+ /**
+ * M4 一键已读
+ * POST /api/v1/message/read-all 入参 {type?}
+ * 规则:将当前分类下的专属未读消息及有效广播全部标记为已读
+ *
+ * @return void
+ */
+ public function readAll()
+ {
+ $user = $this->auth();
+ $uid = (int)$user['id'];
+ $type = $this->input('type', '');
+ $now = time();
+
+ // 1. 批量标记专属个人消息
+ $q = Db::name('fc_message')
+ ->where('user_id', $uid)
+ ->where('isread', 0)
+ ->whereNull('deletetime');
+
+ if ($type !== '' && $type !== null && $type !== 'all') {
+ $q->where('type', $type);
+ }
+ $affected = $q->update([
+ 'isread' => 1,
+ 'readtime' => $now,
+ 'updatetime' => $now,
+ ]);
+
+ // 2. 批量将有效系统广播加入当前用户已读
+ $broadQ = Db::name('fc_message')->where('user_id', 0)->whereNull('deletetime');
+ if ($type !== '' && $type !== null && $type !== 'all') {
+ $broadQ->where('type', $type);
+ }
+ $broadcasts = $broadQ->select();
+ foreach ($broadcasts as $b) {
+ $delUids = array_filter(explode(',', (string)$b['deleted_uids']));
+ if (in_array((string)$uid, $delUids)) {
+ continue;
+ }
+ $readUids = array_filter(explode(',', (string)$b['read_uids']));
+ if (!in_array((string)$uid, $readUids)) {
+ $readUids[] = (string)$uid;
+ Db::name('fc_message')->where('id', $b['id'])->update([
+ 'read_uids' => implode(',', array_unique($readUids)),
+ 'updatetime' => $now,
+ ]);
+ $affected++;
+ }
+ }
+
+ $this->ok(['success' => true, 'affected' => (int)$affected]);
+ }
+
+ /**
+ * M5 一键清理
+ * POST /api/v1/message/clear 入参 {type?}
+ * 规则:对专属通知执行软删除(deletetime);对广播消息记录当前用户已删除集合(deleted_uids)
+ *
+ * @return void
+ */
+ public function clear()
+ {
+ $user = $this->auth();
+ $uid = (int)$user['id'];
+ $type = $this->input('type', '');
+ $now = time();
+
+ // 1. 专属消息软删除
+ $q = Db::name('fc_message')->where('user_id', $uid)->whereNull('deletetime');
+ if ($type !== '' && $type !== null && $type !== 'all') {
+ $q->where('type', $type);
+ }
+ $affected = $q->update([
+ 'deletetime' => $now,
+ 'updatetime' => $now,
+ ]);
+
+ // 2. 广播消息:将 uid 追加到 deleted_uids
+ $broadQ = Db::name('fc_message')->where('user_id', 0)->whereNull('deletetime');
+ if ($type !== '' && $type !== null && $type !== 'all') {
+ $broadQ->where('type', $type);
+ }
+ $broadcasts = $broadQ->select();
+ foreach ($broadcasts as $b) {
+ $delUids = array_filter(explode(',', (string)$b['deleted_uids']));
+ if (!in_array((string)$uid, $delUids)) {
+ $delUids[] = (string)$uid;
+ Db::name('fc_message')->where('id', $b['id'])->update([
+ 'deleted_uids' => implode(',', array_unique($delUids)),
+ 'updatetime' => $now,
+ ]);
+ $affected++;
+ }
+ }
+
+ $this->ok(['success' => true, 'affected' => (int)$affected]);
+ }
+
+ /**
+ * M6 删除单条消息
+ * POST /api/v1/message/:id/delete
+ * 规则:对专属消息软删除(deletetime);对广播消息加入 deleted_uids
+ *
+ * @return void
+ */
+ public function delete()
+ {
+ $user = $this->auth();
+ $uid = (int)$user['id'];
+ $id = (int)$this->request->param('id', 0);
+ $now = time();
+
+ $msg = Db::name('fc_message')->where('id', $id)->whereNull('deletetime')->find();
+ if (!$msg) {
+ $this->fail(404, '消息不存在或已删除');
+ }
+
+ if ((int)$msg['user_id'] === $uid) {
+ // 专属个人消息执行软删除
+ Db::name('fc_message')->where('id', $id)->update([
+ 'deletetime' => $now,
+ 'updatetime' => $now,
+ ]);
+ } elseif ((int)$msg['user_id'] === 0) {
+ // 广播消息:记录已删除该消息的用户 UID
+ $delUids = array_filter(explode(',', (string)$msg['deleted_uids']));
+ if (!in_array((string)$uid, $delUids)) {
+ $delUids[] = (string)$uid;
+ Db::name('fc_message')->where('id', $id)->update([
+ 'deleted_uids' => implode(',', array_unique($delUids)),
+ 'updatetime' => $now,
+ ]);
+ }
+ } else {
+ $this->fail(403, '无权操作此消息');
+ }
+
+ $this->ok(['success' => true]);
}
}
diff --git a/application/api/controller/Post.php b/application/api/controller/Post.php
index 0309314..d53ba0a 100644
--- a/application/api/controller/Post.php
+++ b/application/api/controller/Post.php
@@ -308,7 +308,7 @@ class Post extends V1
// LBS 坐标(gcj02;未定位/为 0 时存 NULL,首页「附近」不参与距离排序)
'lat' => $this->coord('lat', 90),
'lng' => $this->coord('lng', 180),
- 'is_paid' => $paid ? 1 : 0,
+ 'is_paid' => ($paid && $paidNow) ? 1 : 0, // 微信支付发帖未付款前为 0,防止未支付信息外泄
'amount' => $paid ? $amount : 0,
'pay_type' => $paid ? $payType : '',
'audit_status' => 0,
@@ -823,38 +823,77 @@ class Post extends V1
* @param array $post fc_post 行
* @return array
*/
+ /**
+ * 详情页联系方式与内容访问控制(严格执行会员权益与付费拦截)
+ *
+ * 可见规则:
+ * ① 本人发布的信息或已付费解锁记录(fc_pay_view)→ unlocked=true,返回真实联系方式明文
+ * ② 会员享有每日免费查看额度,未点击解锁时不泄露真实号码,下发真实剩余额度与会员标识
+ * ③ 未解锁状态下,对标题与详细正文中的手机号执行掩码遮蔽,封堵正文直接贴电话的白嫖漏洞
+ *
+ * @param array $data buildPost() 结果
+ * @param array $post fc_post 行
+ * @return array 增强后的 Post 详情结构
+ */
protected function contactAccess($data, $post)
{
$data['unlocked'] = false;
$data['wechat'] = '';
$me = $this->user();
- if (!$me) {
- return $data;
+ $now = time();
+ $isMember = false;
+ $quota = $this->memberFreeViewQuota();
+ $used = 0;
+ $freeViewLeft = 0;
+ $payViewPrice = round((float)$this->cfg('fee.payView', 2), 2);
+
+ $isOwner = false;
+ $paid = false;
+
+ if ($me) {
+ $ext = $this->ext($me['id']);
+ $isMember = (int)$ext['is_member'] === 1
+ && (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now);
+ $isOwner = (int)$post['user_id'] === (int)$me['id'];
+ $paid = (bool)Db::name('fc_pay_view')
+ ->where('user_id', $me['id'])
+ ->where('post_id', $post['id'])
+ ->find();
+
+ $today = date('Y-m-d');
+ $used = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
+ $freeViewLeft = $isMember ? max(0, $quota - $used) : 0;
}
- $now = time();
- $ext = $this->ext($me['id']);
- $isMember = (int)$ext['is_member'] === 1
- && (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now);
- $isOwner = (int)$post['user_id'] === (int)$me['id'];
- $paid = (bool)Db::name('fc_pay_view')
- ->where('user_id', $me['id'])
- ->where('post_id', $post['id'])
- ->find();
+ $data['isMember'] = $isMember;
+ $data['freeViewQuota'] = $quota;
+ $data['freeViewUsed'] = $used;
+ $data['freeViewLeft'] = $freeViewLeft;
+ $data['payViewPrice'] = $payViewPrice;
- // 会员每日免费查看剩余次数(只做展示;解锁统一走 payView,按额度/付费计费)
- $quota = $this->memberFreeViewQuota();
- $today = date('Y-m-d');
- $used = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
- $data['freeViewLeft'] = $isMember ? max(0, $quota - $used) : 0;
-
- if ($isOwner || $paid || $isMember) {
- $data['unlocked'] = true;
- $data['phone'] = (string)$post['phone'];
- $data['contact'] = (string)$post['phone'];
+ // 真实解锁态判定:仅本人发布或已生成有效解锁记录
+ if ($isOwner || $paid) {
+ $data['unlocked'] = true;
+ $data['phone'] = (string)$post['phone'];
+ $data['contact'] = (string)$post['phone'];
$data['contactName'] = (string)(isset($post['contact']) ? $post['contact'] : '');
- $data['wechat'] = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $post['id']);
+ $data['wechat'] = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $post['id']);
+ // 若发帖人有关联商家门店,在解锁态下一并展现真实门店电话
+ if (!empty($data['merchant']) && is_array($data['merchant']) && !empty($data['merchant']['id'])) {
+ $rawShopContact = Db::name('fc_merchant')->where('id', $data['merchant']['id'])->value('contact');
+ if ($rawShopContact) {
+ $data['merchant']['contact'] = (string)$rawShopContact;
+ }
+ }
+ } else {
+ // 未解锁态:标题与详细描述中的任何手机号执行动态安全遮蔽,杜绝正文绕过
+ if (!empty($data['content'])) {
+ $data['content'] = $this->maskTextPhones($data['content']);
+ }
+ if (!empty($data['title'])) {
+ $data['title'] = $this->maskTextPhones($data['title']);
+ }
}
return $data;
@@ -923,7 +962,10 @@ class Post extends V1
$id = $this->routeId();
$payType = (string)$this->input('payType', '');
- if (!in_array($payType, ['wxpay', 'balance'], true)) {
+ if ($payType === '') {
+ $payType = 'free';
+ }
+ if (!in_array($payType, ['wxpay', 'balance', 'free'], true)) {
$this->fail('支付方式不支持');
}
@@ -965,6 +1007,9 @@ class Post extends V1
if ($usedFree) {
$amount = 0;
$payType = 'free';
+ } elseif ($payType === 'free') {
+ // 用户希望使用会员免费特权,但额度已耗尽或非会员
+ throw new \RuntimeException($isMember ? '今日会员免费查看额度已用完,请选择支付方式付费查看' : '该内容需付费查看,请选择支付方式');
} elseif ($payType === 'balance') {
// 条件扣款:并发下不会扣成负数
$affected = Db::name('user')
@@ -972,7 +1017,7 @@ class Post extends V1
->where('money', '>=', $amount)
->setDec('money', $amount);
if (!$affected) {
- throw new \RuntimeException('余额不足');
+ throw new \RuntimeException('余额不足,请充值或使用微信支付');
}
$balance = (float)Db::name('user')->where('id', (int)$user['id'])->value('money');
$this->flow('expense', '付费查看', $amount, $balance, $id);
@@ -1019,7 +1064,16 @@ class Post extends V1
$this->ok(['order' => $pending]);
}
- $this->ok(['unlocked' => true, 'contact' => $contact]);
+ $today = date('Y-m-d');
+ $extNow = $this->ext($user['id']);
+ $usedNow = ((string)$extNow['free_view_date'] === $today) ? (int)$extNow['free_view_used'] : 0;
+
+ $this->ok([
+ 'unlocked' => true,
+ 'contact' => $contact,
+ 'freeViewLeft' => $isMember ? max(0, $quota - $usedNow) : 0,
+ 'usedFree' => isset($usedFree) ? $usedFree : false,
+ ]);
}
/**
@@ -1442,7 +1496,7 @@ class Post extends V1
'address' => (string)(isset($m['address']) ? $m['address'] : ''),
'lat' => (isset($m['lat']) && $m['lat'] !== null && $m['lat'] !== '') ? (float)$m['lat'] : null,
'lng' => (isset($m['lng']) && $m['lng'] !== null && $m['lng'] !== '') ? (float)$m['lng'] : null,
- 'contact' => (string)(isset($m['contact']) ? $m['contact'] : ''),
+ 'contact' => $this->maskPhone(isset($m['contact']) ? $m['contact'] : ''), // 默认脱敏,未解锁绝不泄露
'category' => (string)(isset($m['category']) ? $m['category'] : ''),
'notice' => (string)(isset($m['notice']) ? $m['notice'] : ''),
'auditStatus' => (int)$m['audit_status'],
diff --git a/application/api/controller/V1.php b/application/api/controller/V1.php
index 6ee9af4..1510763 100644
--- a/application/api/controller/V1.php
+++ b/application/api/controller/V1.php
@@ -457,11 +457,68 @@ class V1 extends Api
return date('c', (int)$timestamp);
}
- /** 手机号脱敏 */
+ /**
+ * 手机号与电话号码多格式安全脱敏
+ * 兼容标准11位手机号、带+86/0086国际前缀、带空格/中划线及带区号固定电话,防止未付费信息绕过泄露
+ *
+ * @param string $phone 原始号码
+ * @return string 脱敏后的号码字符串
+ */
protected function maskPhone($phone)
{
- $phone = (string)$phone;
- return strlen($phone) === 11 ? substr($phone, 0, 3) . '****' . substr($phone, 7) : $phone;
+ $raw = trim((string)$phone);
+ if ($raw === "") {
+ return "";
+ }
+
+ // 统一提取数字部分进行标准化识别
+ $clean = preg_replace("/[^\d]/", "", $raw);
+
+ // 去除大陆前缀 86 或 0086
+ if (strpos($clean, "86") === 0 && strlen($clean) === 13) {
+ $clean = substr($clean, 2);
+ }
+
+ // 11位大陆标准手机号
+ if (preg_match("/^1[3-9]\d{9}$/", $clean)) {
+ return substr($clean, 0, 3) . "****" . substr($clean, 7);
+ }
+
+ // 10~12位座机固话(如 085188888888、01088888888)
+ if (preg_match("/^0\d{9,11}$/", $clean)) {
+ $prefixLen = (strpos($clean, "01") === 0 || strpos($clean, "02") === 0) ? 3 : 4;
+ return substr($clean, 0, $prefixLen) . "****" . substr($clean, -3);
+ }
+
+ // 其它>=7位的任意连续数字号码,统一对中段进行掩码
+ $len = strlen($clean);
+ if ($len >= 7) {
+ $start = max(1, (int)floor(($len - 4) / 2));
+ return substr($clean, 0, $start) . "****" . substr($clean, $start + 4);
+ }
+
+ return $raw;
+ }
+
+ /**
+ * 文本内容中的手机号与电话动态遮蔽
+ * 用于未付费解锁状态下对标题、详细描述中的手机号执行掩码保护,杜绝发帖人在正文直接贴电话白嫖
+ *
+ * @param string $content 原始富文本或正文内容
+ * @return string 动态掩码后的文本
+ */
+ protected function maskTextPhones($content)
+ {
+ $text = (string)$content;
+ if ($text === "") {
+ return "";
+ }
+
+ // 匹配各类11位大陆手机号码,含可能夹带的空格或横杠(如 138 0000 0000 或 138-0000-0000)
+ $pattern = '/(?:(?:\+|00)86[\s-]?)?(1[3-9]\d)[\s-]?(\d{4})[\s-]?(\d{4})/u';
+ return preg_replace_callback($pattern, function ($matches) {
+ return $matches[1] . '****' . $matches[3];
+ }, $text);
}
/** 生成业务单号(前缀 + 时间 + 8 位随机十六进制,避免同秒并发碰撞) */
diff --git a/application/common/library/OrderFulfill.php b/application/common/library/OrderFulfill.php
index f2ca6e4..4567d47 100644
--- a/application/common/library/OrderFulfill.php
+++ b/application/common/library/OrderFulfill.php
@@ -45,10 +45,34 @@ class OrderFulfill
self::unlockPayView($order);
}
- // post 及其它类型:信息已落库,仅需结算分销
+ if ($type === 'post') {
+ self::fulfillPost($order);
+ }
+
+ // 其它消费类型统一结算分销
DistributionService::settleOrder($order);
}
+ /**
+ * 付费发帖支付成功后激活支付状态
+ * 将 fc_post 的 is_paid 置为 1,确保未付款信息不会流出展示
+ *
+ * @param array $order
+ * @return void
+ */
+ protected static function fulfillPost(array $order)
+ {
+ $postId = (int)(isset($order['related_id']) ? $order['related_id'] : 0);
+ if ($postId <= 0) {
+ return;
+ }
+
+ Db::name('fc_post')->where('id', $postId)->update([
+ 'is_paid' => 1,
+ 'updatetime' => time(),
+ ]);
+ }
+
/**
* 付费查看解锁(user_id + post_id 唯一索引保证幂等)
*
diff --git a/application/route.php b/application/route.php
index 35e8fa0..b1eb482 100755
--- a/application/route.php
+++ b/application/route.php
@@ -101,7 +101,10 @@ return [
/* =============== 六、用户端 · 消息中心(Module-MESSAGE) =============== */
'api/v1/message/list' => 'api/Message/list',
'api/v1/message/unread-count' => 'api/Message/unreadCount',
- 'api/v1/message/:id/read' => 'api/Message/read',
+ 'api/v1/message/read-all' => 'api/Message/readAll',
+ 'api/v1/message/clear' => 'api/Message/clear',
+ 'api/v1/message/:id/read' => 'api/Message/read',
+ 'api/v1/message/:id/delete' => 'api/Message/delete',
/* =============== 六·B、用户端 · 交易与支付(充值 / 微信支付 / 优惠券) ===============
* · pay/notify 为微信支付异步通知:**公开路由,无需登录**,验签与订单置为 paid 由
diff --git a/public/assets/js/backend/fc/message.js b/public/assets/js/backend/fc/message.js
index b8d952f..faf2b43 100644
--- a/public/assets/js/backend/fc/message.js
+++ b/public/assets/js/backend/fc/message.js
@@ -11,6 +11,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
del_url: 'fc/message/del',
multi_url: 'fc/message/multi',
import_url: 'fc/message/import',
+ recyclebin_url: 'fc/message/recyclebin',
table: 'fc_message',
}
});
diff --git a/public/static/icons/cat-car.png b/public/static/icons/cat-car.png
index c707b43..5d7e437 100644
Binary files a/public/static/icons/cat-car.png and b/public/static/icons/cat-car.png differ
diff --git a/public/static/icons/cat-clean.png b/public/static/icons/cat-clean.png
index e301848..86eb846 100644
Binary files a/public/static/icons/cat-clean.png and b/public/static/icons/cat-clean.png differ
diff --git a/public/static/icons/cat-education.png b/public/static/icons/cat-education.png
index 7b98137..759024c 100644
Binary files a/public/static/icons/cat-education.png and b/public/static/icons/cat-education.png differ
diff --git a/public/static/icons/cat-goods-v2.png b/public/static/icons/cat-goods-v2.png
index b5f1764..58c296a 100644
Binary files a/public/static/icons/cat-goods-v2.png and b/public/static/icons/cat-goods-v2.png differ
diff --git a/public/static/icons/cat-goods.png b/public/static/icons/cat-goods.png
index b5f1764..58c296a 100644
Binary files a/public/static/icons/cat-goods.png and b/public/static/icons/cat-goods.png differ
diff --git a/public/static/icons/cat-house.png b/public/static/icons/cat-house.png
index f204ca7..03fb5c3 100644
Binary files a/public/static/icons/cat-house.png and b/public/static/icons/cat-house.png differ
diff --git a/public/static/icons/cat-job.png b/public/static/icons/cat-job.png
index fde0f26..c99f7c3 100644
Binary files a/public/static/icons/cat-job.png and b/public/static/icons/cat-job.png differ
diff --git a/public/static/icons/cat-service.png b/public/static/icons/cat-service.png
index cfe0266..3fe8f7f 100644
Binary files a/public/static/icons/cat-service.png and b/public/static/icons/cat-service.png differ
diff --git a/public/static/icons/edit.png b/public/static/icons/edit.png
index 34920bf..d0d1641 100644
Binary files a/public/static/icons/edit.png and b/public/static/icons/edit.png differ
diff --git a/public/static/icons/home-bell.png b/public/static/icons/home-bell.png
index 71c480a..b4d153a 100644
Binary files a/public/static/icons/home-bell.png and b/public/static/icons/home-bell.png differ
diff --git a/public/static/icons/home-location.png b/public/static/icons/home-location.png
index 7e6181b..9a70053 100644
Binary files a/public/static/icons/home-location.png and b/public/static/icons/home-location.png differ
diff --git a/public/static/icons/info.png b/public/static/icons/info.png
index f878a62..481c29e 100644
Binary files a/public/static/icons/info.png and b/public/static/icons/info.png differ
diff --git a/public/static/icons/link-recharge.png b/public/static/icons/link-recharge.png
index 9ac7e3a..64e9f94 100644
Binary files a/public/static/icons/link-recharge.png and b/public/static/icons/link-recharge.png differ
diff --git a/public/static/icons/location.png b/public/static/icons/location.png
index 30e2ab2..484dfe9 100644
Binary files a/public/static/icons/location.png and b/public/static/icons/location.png differ
diff --git a/public/static/icons/mine-about.png b/public/static/icons/mine-about.png
index dfd8ab8..a1b8fd7 100644
Binary files a/public/static/icons/mine-about.png and b/public/static/icons/mine-about.png differ
diff --git a/public/static/icons/mine-dist.png b/public/static/icons/mine-dist.png
index d5d7e97..a10d65f 100644
Binary files a/public/static/icons/mine-dist.png and b/public/static/icons/mine-dist.png differ
diff --git a/public/static/icons/mine-draft.png b/public/static/icons/mine-draft.png
index df81ff4..05e7b12 100644
Binary files a/public/static/icons/mine-draft.png and b/public/static/icons/mine-draft.png differ
diff --git a/public/static/icons/mine-follow.png b/public/static/icons/mine-follow.png
index 636a69c..d094c3d 100644
Binary files a/public/static/icons/mine-follow.png and b/public/static/icons/mine-follow.png differ
diff --git a/public/static/icons/mine-history.png b/public/static/icons/mine-history.png
index b56e7bc..2737f0d 100644
Binary files a/public/static/icons/mine-history.png and b/public/static/icons/mine-history.png differ
diff --git a/public/static/icons/mine-order.png b/public/static/icons/mine-order.png
index b5f1764..d18c333 100644
Binary files a/public/static/icons/mine-order.png and b/public/static/icons/mine-order.png differ
diff --git a/public/static/icons/mine-post.png b/public/static/icons/mine-post.png
index 0c931a4..475830f 100644
Binary files a/public/static/icons/mine-post.png and b/public/static/icons/mine-post.png differ
diff --git a/public/static/icons/mine-setting.png b/public/static/icons/mine-setting.png
index 3f2be2c..ee64619 100644
Binary files a/public/static/icons/mine-setting.png and b/public/static/icons/mine-setting.png differ
diff --git a/public/static/icons/paywechat.png b/public/static/icons/paywechat.png
index 5d3f99a..5925c7e 100644
Binary files a/public/static/icons/paywechat.png and b/public/static/icons/paywechat.png differ
diff --git a/public/static/icons/plus-ink.png b/public/static/icons/plus-ink.png
index 849f277..d4907ce 100644
Binary files a/public/static/icons/plus-ink.png and b/public/static/icons/plus-ink.png differ
diff --git a/public/static/icons/shield.png b/public/static/icons/shield.png
index a77a78e..eaa645f 100644
Binary files a/public/static/icons/shield.png and b/public/static/icons/shield.png differ
diff --git a/public/static/icons/tab-category-on.png b/public/static/icons/tab-category-on.png
index 8badb4a..ea4a4b5 100644
Binary files a/public/static/icons/tab-category-on.png and b/public/static/icons/tab-category-on.png differ
diff --git a/public/static/icons/tab-home-on.png b/public/static/icons/tab-home-on.png
index bdb37d0..3593a54 100644
Binary files a/public/static/icons/tab-home-on.png and b/public/static/icons/tab-home-on.png differ
diff --git a/public/static/icons/tab-message-on.png b/public/static/icons/tab-message-on.png
index 99e0a5f..cfc3737 100644
Binary files a/public/static/icons/tab-message-on.png and b/public/static/icons/tab-message-on.png differ
diff --git a/public/static/icons/tab-mine-on.png b/public/static/icons/tab-mine-on.png
index 2fa95a7..414179f 100644
Binary files a/public/static/icons/tab-mine-on.png and b/public/static/icons/tab-mine-on.png differ
diff --git a/public/static/icons/tab-post-on.png b/public/static/icons/tab-post-on.png
index 92e7f92..67144b9 100644
Binary files a/public/static/icons/tab-post-on.png and b/public/static/icons/tab-post-on.png differ
diff --git a/tools/analyze_integrity.php b/tools/analyze_integrity.php
new file mode 100644
index 0000000..a6ebf56
--- /dev/null
+++ b/tools/analyze_integrity.php
@@ -0,0 +1,58 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ echo "=== 1. 所有发布帖子的 user_id 在 fa_user 中是否存在 ===" . PHP_EOL;
+ $postUsers = $pdo->query("SELECT DISTINCT user_id FROM fa_fc_post")->fetchAll(PDO::FETCH_COLUMN);
+ foreach ($postUsers as $uid) {
+ $user = $pdo->query("SELECT * FROM fa_user WHERE id = $uid")->fetch(PDO::FETCH_ASSOC);
+ if (!$user) {
+ echo "【警告】Post 的 user_id={$uid} 在 fa_user 中不存在!" . PHP_EOL;
+ } else {
+ $ext = $pdo->query("SELECT * FROM fa_fc_user_ext WHERE user_id = $uid")->fetch(PDO::FETCH_ASSOC);
+ $merchant = $pdo->query("SELECT * FROM fa_fc_merchant WHERE user_id = $uid")->fetch(PDO::FETCH_ASSOC);
+ echo "Post user_id: {$uid} | 用户名: {$user['username']} | 昵称: {$user['nickname']} | 头像: " . ($user['avatar'] ?: '【空】') . " | 手机: " . ($user['mobile'] ?: '【空】') . " | user_ext: " . ($ext ? "有(type={$ext['user_type']})" : '【无】') . " | merchant: " . ($merchant ? "有(name={$merchant['name']}, logo=" . ($merchant['logo'] ?: '【空】') . ")" : '无') . PHP_EOL;
+ }
+ }
+
+ echo PHP_EOL . "=== 2. 查看 fa_fc_merchant 现有所有商家的详情 ===" . PHP_EOL;
+ $merchants = $pdo->query("SELECT * FROM fa_fc_merchant")->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($merchants as $m) {
+ echo "Merchant ID: {$m['id']}, user_id: {$m['user_id']}, name: {$m['name']}, contact: {$m['contact']}, logo: " . ($m['logo'] ?: '【空】') . ", banner: " . ($m['banner'] ?: '【空】') . ", address: " . ($m['address'] ?: '【空】') . ", lat: {$m['lat']}, lng: {$m['lng']}, category: {$m['category']}, shop_status: {$m['shop_status']}, audit_status: {$m['audit_status']}" . PHP_EOL;
+ }
+
+ echo PHP_EOL . "=== 3. 检查 fa_fc_post 中各帖子的 identity_type 与 user/merchant 匹配情况 ===" . PHP_EOL;
+ $posts = $pdo->query("SELECT p.id, p.user_id, p.category_id, p.identity_type, p.title, p.contact, p.phone, u.nickname, ue.user_type FROM fa_fc_post p LEFT JOIN fa_user u ON p.user_id = u.id LEFT JOIN fa_fc_user_ext ue ON p.user_id = ue.user_id WHERE p.deletetime IS NULL")->fetchAll(PDO::FETCH_ASSOC);
+ $inconsistent = [];
+ foreach ($posts as $p) {
+ $m = $pdo->query("SELECT * FROM fa_fc_merchant WHERE user_id = {$p['user_id']}")->fetch(PDO::FETCH_ASSOC);
+ $isMerchantUser = ($p['user_type'] == 2 || $m);
+ if ($isMerchantUser && $p['identity_type'] != 2) {
+ $inconsistent[] = "Post #{$p['id']} '{$p['title']}' (user_id={$p['user_id']}, {$p['nickname']}) 是商家身份但 identity_type={$p['identity_type']}";
+ }
+ }
+ echo "商家发布但 identity_type 为个人的帖子数量: " . count($inconsistent) . PHP_EOL;
+ foreach (array_slice($inconsistent, 0, 10) as $msg) {
+ echo " " . $msg . PHP_EOL;
+ }
+
+ echo PHP_EOL . "=== 4. 检查用户表中的头像和手机号情况 ===" . PHP_EOL;
+ $users = $pdo->query("SELECT * FROM fa_user")->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($users as $u) {
+ $issues = [];
+ if (empty($u['avatar'])) $issues[] = "无头像";
+ if (empty($u['mobile'])) $issues[] = "无手机";
+ if (empty($u['nickname']) || strpos($u['nickname'], '同城用户') === 0) $issues[] = "昵称弱({$u['nickname']})";
+ if (!empty($issues)) {
+ echo "User #{$u['id']} ({$u['username']}): " . implode(', ', $issues) . PHP_EOL;
+ }
+ }
+}
+
+analyzeIntegrity();
diff --git a/tools/banner1.html b/tools/banner1.html
new file mode 100644
index 0000000..e2ca551
--- /dev/null
+++ b/tools/banner1.html
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
同城优选 · 真实验货
+
同城好物节 二手直降
+
闲置数码家居 · 当面验货超省心 · 邻里自提
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/banner2.html b/tools/banner2.html
new file mode 100644
index 0000000..da92033
--- /dev/null
+++ b/tools/banner2.html
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
名企急招 · 官方认证
+
同城高薪直聘 急速入职
+
海量好岗任选 · 直接聊老板 · 真实高薪看得见
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/banner3.html b/tools/banner3.html
new file mode 100644
index 0000000..88213ac
--- /dev/null
+++ b/tools/banner3.html
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
准时上门 · 售后无忧
+
本地便民到家 贴心省心
+
家庭保洁开荒 · 维修拆洗搬家 · 专业师傅随叫随到
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/banners_render.html b/tools/banners_render.html
new file mode 100644
index 0000000..0c8540b
--- /dev/null
+++ b/tools/banners_render.html
@@ -0,0 +1,398 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
同城优选 · 真实验货
+
同城好物节 二手直降
+
闲置数码家居 · 当面验货超省心 · 邻里自提
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
真实企业 · 官方认证
+
同城高薪直聘 急速入职
+
海量好岗任选 · 直接聊老板 · 薪资透明全覆盖
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
准时上门 · 售后保障
+
本地便民到家 贴心无忧
+
家政保洁开荒 · 管道维修家电拆洗 · 随叫随到
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/generate_assets.php b/tools/generate_assets.php
new file mode 100644
index 0000000..d86f397
--- /dev/null
+++ b/tools/generate_assets.php
@@ -0,0 +1,196 @@
+ [[0, 210, 106], [0, 168, 84]], // 品牌浅翠绿
+ 'mint' => [[16, 185, 129], [5, 150, 105]], // 薄荷青
+ 'blue' => [[59, 130, 246], [37, 99, 235]], // 湖蓝
+ 'orange' => [[245, 158, 11], [217, 119, 6]], // 阳光金橙
+ 'teal' => [[20, 184, 166], [13, 148, 136]], // 青碧
+ 'purple' => [[139, 92, 246], [124, 58, 237]], // 浅紫
+ 'rose' => [[244, 63, 94], [225, 29, 72]], // 珊瑚粉
+ 'slate' => [[100, 116, 139], [71, 85, 105]], // 雅灰
+ ];
+
+ /**
+ * 生成带首字徽标的现代化圆形/微圆角PNG头像
+ *
+ * @param string $filePath 输出文件绝对路径
+ * @param string $char 显示的文字(1个汉字或英文)
+ * @param array $theme 配色方案
+ * @param int $size 图片尺寸
+ * @param bool $isSquare 是否为商家微圆角方形
+ * @return void
+ */
+ $createBadge = function ($filePath, $char, $theme, $size = 128, $isSquare = false) {
+ $img = imagecreatetruecolor($size, $size);
+ imagealphablending($img, false);
+ imagesavealpha($img, true);
+
+ $trans = imagecolorallocatealpha($img, 0, 0, 0, 127);
+ imagefill($img, 0, 0, $trans);
+
+ $c1 = $theme[0];
+ $c2 = $theme[1];
+
+ // 绘制渐变背景
+ for ($y = 0; $y < $size; $y++) {
+ $ratio = $y / $size;
+ $r = (int)($c1[0] * (1 - $ratio) + $c2[0] * $ratio);
+ $g = (int)($c1[1] * (1 - $ratio) + $c2[1] * $ratio);
+ $b = (int)($c1[2] * (1 - $ratio) + $c2[2] * $ratio);
+ $col = imagecolorallocate($img, $r, $g, $b);
+
+ for ($x = 0; $x < $size; $x++) {
+ if ($isSquare) {
+ // 圆角矩形(圆角半径 24)
+ $rad = 24;
+ $in = true;
+ if ($x < $rad && $y < $rad) {
+ $in = (($x - $rad) * ($x - $rad) + ($y - $rad) * ($y - $rad)) <= ($rad * $rad);
+ } elseif ($x >= ($size - $rad) && $y < $rad) {
+ $in = (($x - ($size - $rad)) * ($x - ($size - $rad)) + ($y - $rad) * ($y - $rad)) <= ($rad * $rad);
+ } elseif ($x < $rad && $y >= ($size - $rad)) {
+ $in = (($x - $rad) * ($x - $rad) + ($y - ($size - $rad)) * ($y - ($size - $rad))) <= ($rad * $rad);
+ } elseif ($x >= ($size - $rad) && $y >= ($size - $rad)) {
+ $in = (($x - ($size - $rad)) * ($x - ($size - $rad)) + ($y - ($size - $rad)) * ($y - ($size - $rad))) <= ($rad * $rad);
+ }
+ if ($in) {
+ imagesetpixel($img, $x, $y, $col);
+ }
+ } else {
+ // 圆形头像
+ $cx = $size / 2;
+ $cy = $size / 2;
+ $distSq = ($x - $cx) * ($x - $cx) + ($y - $cy) * ($y - $cy);
+ if ($distSq <= ($size / 2) * ($size / 2)) {
+ imagesetpixel($img, $x, $y, $col);
+ }
+ }
+ }
+ }
+
+ imagealphablending($img, true);
+
+ // 尝试加载系统常见中文字体,若无则使用 imagestring 兜底
+ $fontCandidates = [
+ 'C:\\Windows\\Fonts\\msyh.ttc', // 微软雅黑
+ 'C:\\Windows\\Fonts\\msyh.ttf',
+ 'C:\\Windows\\Fonts\\simhei.ttf', // 黑体
+ 'C:\\Windows\\Fonts\\simsun.ttc',
+ ];
+ $fontFile = null;
+ foreach ($fontCandidates as $f) {
+ if (file_exists($f)) {
+ $fontFile = $f;
+ break;
+ }
+ }
+
+ $white = imagecolorallocate($img, 255, 255, 255);
+ if ($fontFile) {
+ $fontSize = $isSquare ? 46 : 42;
+ $box = imagettfbbox($fontSize, 0, $fontFile, $char);
+ $tw = $box[2] - $box[0];
+ $th = $box[1] - $box[7];
+ $tx = (int)(($size - $tw) / 2) - $box[0];
+ $ty = (int)(($size - $th) / 2) - $box[7];
+ imagettftext($img, $fontSize, 0, $tx, $ty, $white, $fontFile, $char);
+ } else {
+ $fSize = 5;
+ $tw = imagefontwidth($fSize) * strlen($char);
+ $th = imagefontheight($fSize);
+ $tx = (int)(($size - $tw) / 2);
+ $ty = (int)(($size - $th) / 2);
+ imagestring($img, $fSize, $tx, $ty, $char, $white);
+ }
+
+ imagepng($img, $filePath, 6);
+ imagedestroy($img);
+ };
+
+ // 1. 生成用户头像列表
+ $userAvatars = [
+ 'user_1001.png' => ['助', $palette['green']],
+ 'user_1002.png' => ['王', $palette['blue']],
+ 'user_1003.png' => ['洁', $palette['teal']],
+ 'user_1004.png' => ['测', $palette['slate']],
+ 'user_2001.png' => ['顺', $palette['orange']],
+ 'user_2002.png' => ['安', $palette['green']],
+ 'user_2003.png' => ['骑', $palette['rose']],
+ 'user_2004.png' => ['车', $palette['blue']],
+ 'user_2005.png' => ['净', $palette['teal']],
+ 'user_2006.png' => ['豪', $palette['purple']],
+ 'user_2007.png' => ['优', $palette['orange']],
+ 'user_2008.png' => ['琴', $palette['rose']],
+ 'user_2009.png' => ['筑', $palette['green']],
+ 'user_2010.png' => ['黔', $palette['blue']],
+ 'user_2027.png' => ['林', $palette['teal']],
+ 'user_2028.png' => ['超', $palette['orange']],
+ 'user_default.png' => ['同', $palette['green']],
+ ];
+
+ foreach ($userAvatars as $fileName => $cfg) {
+ $path = $avatarDir . '/' . $fileName;
+ $createBadge($path, $cfg[0], $cfg[1], 128, false);
+ }
+ echo "已成功生成 " . count($userAvatars) . " 个用户头像到 public/uploads/avatars/" . PHP_EOL;
+
+ // 2. 生成商家 Logo 列表
+ $merchantLogos = [
+ 'm_shunjie.png' => ['顺', $palette['orange']],
+ 'm_anjia.png' => ['安', $palette['green']],
+ 'm_jiejing.png' => ['洁', $palette['teal']],
+ 'm_chehang.png' => ['车', $palette['blue']],
+ 'm_youxuan.png' => ['优', $palette['purple']],
+ 'm_zaowang.png' => ['枣', $palette['rose']],
+ ];
+
+ foreach ($merchantLogos as $fileName => $cfg) {
+ $path = $merchantDir . '/' . $fileName;
+ $createBadge($path, $cfg[0], $cfg[1], 128, true);
+ }
+ echo "已成功生成 " . count($merchantLogos) . " 个商家Logo到 public/uploads/merchants/" . PHP_EOL;
+
+ // 3. 生成商家营业执照模拟图
+ $licenseImg = imagecreatetruecolor(400, 280);
+ $bg = imagecolorallocate($licenseImg, 252, 250, 242); // 羊皮纸色
+ imagefill($licenseImg, 0, 0, $bg);
+ $border = imagecolorallocate($licenseImg, 180, 160, 120);
+ imagerectangle($licenseImg, 10, 10, 389, 269, $border);
+ imagerectangle($licenseImg, 14, 14, 385, 265, $border);
+ $textDark = imagecolorallocate($licenseImg, 50, 50, 50);
+ $red = imagecolorallocate($licenseImg, 220, 38, 38);
+
+ $fontFile = 'C:\\Windows\\Fonts\\msyh.ttc';
+ if (file_exists($fontFile)) {
+ imagettftext($licenseImg, 18, 0, 140, 60, $textDark, $fontFile, '营业执照');
+ imagettftext($licenseImg, 10, 0, 40, 100, $textDark, $fontFile, '统一社会信用代码:91520102MA6XYZ8888');
+ imagettftext($licenseImg, 10, 0, 40, 130, $textDark, $fontFile, '类型:有限责任公司(自然人独资)');
+ imagettftext($licenseImg, 10, 0, 40, 160, $textDark, $fontFile, '登记机关:贵阳市市场监督管理局');
+ imagettftext($licenseImg, 10, 0, 40, 190, $textDark, $fontFile, '经营状态:开业(官方核验通过)');
+ // 模拟红色印章
+ imageellipse($licenseImg, 320, 200, 60, 60, $red);
+ imagettftext($licenseImg, 9, 0, 298, 204, $red, $fontFile, '官方核验');
+ }
+ imagepng($licenseImg, $merchantDir . '/license_default.png', 6);
+ imagedestroy($licenseImg);
+ echo "已成功生成营业执照模拟图到 public/uploads/merchants/license_default.png" . PHP_EOL;
+}
+
+generateAvatarsAndLogos();
diff --git a/tools/repair_database_data.php b/tools/repair_database_data.php
new file mode 100644
index 0000000..4607f0e
--- /dev/null
+++ b/tools/repair_database_data.php
@@ -0,0 +1,428 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ $now = time();
+
+ echo "==================================================" . PHP_EOL;
+ echo "步骤 1: 修复与补全 fa_user 用户表测试数据" . PHP_EOL;
+ echo "==================================================" . PHP_EOL;
+
+ // 用户定义列表:[id, username, nickname, mobile, avatar, gender, bio]
+ $usersConfig = [
+ 1001 => ['fc1001', '同城小助手', '13800001111', '/uploads/avatars/user_1001.png', 1, '同城便民服务与官方信息发布助手'],
+ 1002 => ['fc1002', '小王同学', '15033443344', '/uploads/avatars/user_1002.png', 1, '贵阳本地在校大学生,分享生活闲置与学习资讯'],
+ 1003 => ['fc1003', '洁净到家·陈姐', '13156785678', '/uploads/avatars/user_1003.png', 2, '10年家政保洁经验,诚信服务千家万户'],
+ 1004 => ['fc1004', '测试用户A', '13000000000', '/uploads/avatars/user_1004.png', 1, '同城测试体验账号'],
+ 2001 => ['fc2001', '顺捷物流·杨经理', '13988888888', '/uploads/avatars/user_2001.png', 1, '顺捷同城物流调度中心,高效货运日结保障'],
+ 2002 => ['fc2002', '安家房产·周主管', '13566666666', '/uploads/avatars/user_2002.png', 1, '安家房产资深经纪人,真实房源无中介费陷阱'],
+ 2003 => ['fc2003', '骑行爱好者·小周', '13712341234', '/uploads/avatars/user_2003.png', 1, '周末黔灵山与花溪十里河滩常客,爱骑行爱运动'],
+ 2004 => ['fc2004', '车行天下·张总', '13800000000', '/uploads/avatars/user_2004.png', 1, '专注高品质二手车买卖与全车深度质检'],
+ 2005 => ['fc2005', '洁净到家·王师傅', '13156785678', '/uploads/avatars/user_2005.png', 1, '深度家电清洗、管道开荒,专业设备上门'],
+ 2006 => ['fc2006', '数码达人·阿豪', '15033443344', '/uploads/avatars/user_2006.png', 1, '自用数码产品转让,品质如新,支持面交验机'],
+ 2007 => ['fc2007', '优选二手车·赵经理', '13700000000', '/uploads/avatars/user_2007.png', 1, '品质二手私家车,无事故支持第三方检测'],
+ 2008 => ['wx1a5fb068c3', '琴海飞扬', '13618512779', '/uploads/avatars/user_2008.png', 2, '热爱音乐与摄影,发现贵阳同城美好'],
+ 2009 => ['wx5d0e3c36ba', '筑城漫步', '13785104643', '/uploads/avatars/user_2009.png', 2, '闲置物品断舍离,环保慢生活'],
+ 2010 => ['wxc7bb372973', '黔中书虫', '13885106507', '/uploads/avatars/user_2010.png', 1, '书香满屋,同城爱书人常交流'],
+ 2011 => ['wx40cf7d49bf', '枣王府·王鹏', '18885795042', '/uploads/merchants/m_zaowang.png', 1, '南明区地道餐饮老店,味美价实'],
+ 2012 => ['wxedc3a78f3a', '黔山行者', '13985106138', '/uploads/avatars/user_default.png', 1, '周末爬山徒步搭子'],
+ 2025 => ['wxef1d83d35a', '灵秀筑城', '13685106579', '/uploads/avatars/user_default.png', 2, '贵阳本地美食与生活分享'],
+ 2026 => ['wx0e2dfdf782', '贵山小友', '13785109432', '/uploads/avatars/user_default.png', 1, '摄影搭子与同城骑行'],
+ 2027 => ['u18386351589', '林晓峰', '18386351589', '/uploads/avatars/user_2027.png', 1, '数码数车爱好者,同城真实发布'],
+ 2028 => ['wx568d124539', '机车阿超', '18386351589', '/uploads/avatars/user_2028.png', 1, '春风800nk车主,机车骑行交流'],
+ ];
+
+ $stmtUpdateUser = $pdo->prepare("UPDATE fa_user SET nickname = :nickname, mobile = :mobile, avatar = :avatar, gender = :gender, bio = :bio, updatetime = :updatetime WHERE id = :id");
+
+ foreach ($usersConfig as $uid => $cfg) {
+ $chk = $pdo->query("SELECT id FROM fa_user WHERE id = $uid")->fetch();
+ if ($chk) {
+ $stmtUpdateUser->execute([
+ ':nickname' => $cfg[1],
+ ':mobile' => $cfg[2],
+ ':avatar' => $cfg[3],
+ ':gender' => $cfg[4],
+ ':bio' => $cfg[5],
+ ':updatetime' => $now,
+ ':id' => $uid,
+ ]);
+ echo "已完善用户 ID {$uid} ({$cfg[1]})" . PHP_EOL;
+ }
+ }
+
+ // 针对其它遗留未设头像的普通用户,设置默认头像兜底
+ $pdo->query("UPDATE fa_user SET avatar = '/uploads/avatars/user_default.png' WHERE avatar IS NULL OR avatar = ''");
+ echo "已为其余未设头像的用户填充默认头像。" . PHP_EOL;
+
+ echo PHP_EOL . "==================================================" . PHP_EOL;
+ echo "步骤 2: 修复与补全 fa_fc_user_ext 用户扩展表数据" . PHP_EOL;
+ echo "==================================================" . PHP_EOL;
+
+ // 扩展表配置:user_id => [user_type (1个人 2商家), realname, is_member]
+ $extConfig = [
+ 1001 => [1, '同城小助手', 1],
+ 1002 => [1, '王梓轩', 0],
+ 1003 => [2, '陈秀英', 1],
+ 1004 => [1, '测试员', 0],
+ 2001 => [2, '杨顺捷', 1],
+ 2002 => [2, '周安家', 1],
+ 2003 => [1, '周逸尘', 0],
+ 2004 => [2, '张车行', 1],
+ 2005 => [2, '王保洁', 1],
+ 2006 => [1, '黄子豪', 0],
+ 2007 => [2, '赵优选', 1],
+ 2008 => [1, '琴飞飞', 0],
+ 2009 => [1, '漫小筑', 0],
+ 2010 => [1, '黔晓书', 0],
+ 2011 => [2, '王鹏', 1],
+ 2027 => [1, '林晓峰', 0],
+ 2028 => [1, '王超', 1],
+ ];
+
+ foreach ($extConfig as $uid => $cfg) {
+ $ext = $pdo->query("SELECT id FROM fa_fc_user_ext WHERE user_id = $uid")->fetch();
+ if ($ext) {
+ $stmt = $pdo->prepare("UPDATE fa_fc_user_ext SET user_type = :user_type, realname = :realname, realname_status = 1, is_member = :is_member, updatetime = :now WHERE user_id = :uid");
+ $stmt->execute([
+ ':user_type' => $cfg[0],
+ ':realname' => $cfg[1],
+ ':is_member' => $cfg[2],
+ ':now' => $now,
+ ':uid' => $uid,
+ ]);
+ } else {
+ $stmt = $pdo->prepare("INSERT INTO fa_fc_user_ext (user_id, user_type, realname, realname_status, is_member, post_quota_left, post_quota_expire, createtime, updatetime) VALUES (:uid, :user_type, :realname, 1, :is_member, 10, :expire, :now, :now)");
+ $stmt->execute([
+ ':uid' => $uid,
+ ':user_type' => $cfg[0],
+ ':realname' => $cfg[1],
+ ':is_member' => $cfg[2],
+ ':expire' => $now + 86400 * 30,
+ ':now' => $now,
+ ]);
+ }
+ echo "已同步扩展资料 user_id: {$uid} (身份: " . ($cfg[0] === 2 ? '商家' : '个人') . ", 实名: {$cfg[1]})" . PHP_EOL;
+ }
+
+ echo PHP_EOL . "==================================================" . PHP_EOL;
+ echo "步骤 3: 修复与补全 fa_fc_merchant 商家表档案数据" . PHP_EOL;
+ echo "==================================================" . PHP_EOL;
+
+ // 商家详细配置列表
+ $merchantsConfig = [
+ 2001 => [
+ 'name' => '顺捷同城物流园',
+ 'logo' => '/uploads/merchants/m_shunjie.png',
+ 'banner' => '/static/posts/job-warehouse.jpg',
+ 'notice' => '专业同城配送、日结货运搬家、冷链中转,全天候随叫随到,高效保质!',
+ 'recommend' => 1,
+ 'contact' => '杨经理 13988888888',
+ 'address' => '贵州省贵阳市南明区富源南路108号顺捷物流园A区',
+ 'lat' => 26.542100,
+ 'lng' => 106.732100,
+ 'category' => '同城货运',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ 2002 => [
+ 'name' => '安家同城房产置业',
+ 'logo' => '/uploads/merchants/m_anjia.png',
+ 'banner' => '/static/posts/house-exterior.jpg',
+ 'notice' => '贵阳全城真实房源、精装整租/合租、二手房免佣买卖,资深经纪人1对1带看。',
+ 'recommend' => 1,
+ 'contact' => '周主管 13566666666',
+ 'address' => '贵州省贵阳市云岩区北京路128号安家房产置业中心',
+ 'lat' => 26.598200,
+ 'lng' => 106.708300,
+ 'category' => '房产中介',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ 2005 => [
+ 'name' => '洁净到家品质家政',
+ 'logo' => '/uploads/merchants/m_jiejing.png',
+ 'banner' => '/static/posts/clean-window.jpg',
+ 'notice' => '专业家庭日常保洁、新房开荒保洁、油烟机家电深度拆洗、月嫂育儿,持证上岗。',
+ 'recommend' => 1,
+ 'contact' => '王师傅 13156785678',
+ 'address' => '贵州省贵阳市观山湖区林城东路88号洁净生活馆',
+ 'lat' => 26.631500,
+ 'lng' => 106.632800,
+ 'category' => '家政保洁',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ 1003 => [
+ 'name' => '洁净到家·云岩服务中心',
+ 'logo' => '/uploads/merchants/m_jiejing.png',
+ 'banner' => '/static/posts/clean-bath.jpg',
+ 'notice' => '云岩老城区快速响应保洁与钟点服务,价格透明,满意后再付款。',
+ 'recommend' => 1,
+ 'contact' => '陈姐 13156785678',
+ 'address' => '贵州省贵阳市云岩区中华北路56号',
+ 'lat' => 26.589100,
+ 'lng' => 106.713500,
+ 'category' => '家政保洁',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ 2004 => [
+ 'name' => '车行天下名车汇',
+ 'logo' => '/uploads/merchants/m_chehang.png',
+ 'banner' => '/static/posts/car-suv-white.jpg',
+ 'notice' => '精选准新二手私家车、第三方268项专业检测,支持分期置换,一年两万公里质保。',
+ 'recommend' => 1,
+ 'contact' => '张总 13800000000',
+ 'address' => '贵州省贵阳市花溪区花溪大道南段668号车行天下展厅',
+ 'lat' => 26.438200,
+ 'lng' => 106.678900,
+ 'category' => '汽车交易',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ 2007 => [
+ 'name' => '优选二手车精品馆',
+ 'logo' => '/uploads/merchants/m_youxuan.png',
+ 'banner' => '/static/posts/car-sedan-white.jpg',
+ 'notice' => '专注中高端精品一手二手车,杜绝事故车、泡水车,提供全国联保与免费过户。',
+ 'recommend' => 1,
+ 'contact' => '赵经理 13700000000',
+ 'address' => '贵州省贵阳市观山湖区金阳北路218号优选名车馆',
+ 'lat' => 26.619800,
+ 'lng' => 106.621200,
+ 'category' => '汽车交易',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ 2011 => [
+ 'name' => '枣王府地道特色餐饮',
+ 'logo' => '/uploads/merchants/m_zaowang.png',
+ 'banner' => '/static/posts/job-cafe.jpg',
+ 'notice' => '传承贵州传统地道风味,精选特色食材,支持堂食聚会与同城订餐配送。',
+ 'recommend' => 1,
+ 'contact' => '王鹏 18885795042',
+ 'address' => '贵州省贵阳市南明区箭道街52号(南明区人民政府南侧)',
+ 'lat' => 26.568220,
+ 'lng' => 106.714100,
+ 'category' => '美食餐饮',
+ 'license' => '/uploads/merchants/license_default.png',
+ 'shop_status' => 'normal',
+ 'audit_status'=> 1,
+ ],
+ ];
+
+ foreach ($merchantsConfig as $uid => $m) {
+ $exist = $pdo->query("SELECT id FROM fa_fc_merchant WHERE user_id = $uid")->fetch();
+ if ($exist) {
+ $stmt = $pdo->prepare("UPDATE fa_fc_merchant SET
+ name = :name,
+ logo = :logo,
+ banner = :banner,
+ notice = :notice,
+ recommend = :recommend,
+ contact = :contact,
+ address = :address,
+ lat = :lat,
+ lng = :lng,
+ category = :category,
+ license = :license,
+ shop_status = :shop_status,
+ audit_status = :audit_status,
+ updatetime = :now
+ WHERE id = :id");
+ $stmt->execute([
+ ':name' => $m['name'],
+ ':logo' => $m['logo'],
+ ':banner' => $m['banner'],
+ ':notice' => $m['notice'],
+ ':recommend' => $m['recommend'],
+ ':contact' => $m['contact'],
+ ':address' => $m['address'],
+ ':lat' => $m['lat'],
+ ':lng' => $m['lng'],
+ ':category' => $m['category'],
+ ':license' => $m['license'],
+ ':shop_status' => $m['shop_status'],
+ ':audit_status'=> $m['audit_status'],
+ ':now' => $now,
+ ':id' => $exist['id'],
+ ]);
+ echo "已升级更新商家档案: {$m['name']} (ID: {$exist['id']}, user_id: {$uid})" . PHP_EOL;
+ } else {
+ $stmt = $pdo->prepare("INSERT INTO fa_fc_merchant (
+ user_id, name, logo, banner, notice, recommend, contact, address, lat, lng, category, license, audit_status, shop_status, post_count, createtime, updatetime
+ ) VALUES (
+ :uid, :name, :logo, :banner, :notice, :recommend, :contact, :address, :lat, :lng, :category, :license, :audit_status, :shop_status, 0, :now, :now
+ )");
+ $stmt->execute([
+ ':uid' => $uid,
+ ':name' => $m['name'],
+ ':logo' => $m['logo'],
+ ':banner' => $m['banner'],
+ ':notice' => $m['notice'],
+ ':recommend' => $m['recommend'],
+ ':contact' => $m['contact'],
+ ':address' => $m['address'],
+ ':lat' => $m['lat'],
+ ':lng' => $m['lng'],
+ ':category' => $m['category'],
+ ':license' => $m['license'],
+ ':audit_status'=> $m['audit_status'],
+ ':shop_status' => $m['shop_status'],
+ ':now' => $now,
+ ]);
+ echo "已补全新增商家档案: {$m['name']} (user_id: {$uid})" . PHP_EOL;
+ }
+ }
+
+ echo PHP_EOL . "==================================================" . PHP_EOL;
+ echo "步骤 4: 修复与补全 fa_fc_post 帖子信息与身份归属" . PHP_EOL;
+ echo "==================================================" . PHP_EOL;
+
+ // 1. 商家身份列表 (1003, 2001, 2002, 2004, 2005, 2007, 2011)
+ $merchantUserIds = [1003, 2001, 2002, 2004, 2005, 2007, 2011];
+ $inStr = implode(',', $merchantUserIds);
+
+ // 将这些商家账号发布的帖子的 identity_type 修正为 2 (商家)
+ $stmtMerchantPost = $pdo->query("UPDATE fa_fc_post SET identity_type = 2 WHERE user_id IN ($inStr)");
+ $affected = $stmtMerchantPost->rowCount();
+ echo "已纠正 {$affected} 条商家帖子的 identity_type 为 2 (商家身份)。" . PHP_EOL;
+
+ // 其余个人账号 (如 1001, 1002, 2003, 2006, 2008, 2009, 2010, 2027, 2028) 确保为 1 (个人)
+ $stmtPersonPost = $pdo->query("UPDATE fa_fc_post SET identity_type = 1 WHERE user_id NOT IN ($inStr)");
+ echo "已确认个人帖子的 identity_type 为 1。" . PHP_EOL;
+
+ // 2. 检查并补全每条帖子的 contact, phone, wechat, address, lat, lng
+ $posts = $pdo->query("SELECT p.*, u.nickname, u.mobile FROM fa_fc_post p LEFT JOIN fa_user u ON p.user_id = u.id WHERE p.deletetime IS NULL")->fetchAll(PDO::FETCH_ASSOC);
+
+ $areaDefaultCoord = [
+ '南明区' => [26.568, 106.716, '贵阳市南明区花果园大街'],
+ '云岩区' => [26.605, 106.710, '贵阳市云岩区北京路128号'],
+ '观山湖区' => [26.625, 106.625, '贵阳市观山湖区金融城金阳北路'],
+ '花溪区' => [26.430, 106.670, '贵阳市花溪区花溪大道南段'],
+ ];
+
+ $stmtFixPost = $pdo->prepare("UPDATE fa_fc_post SET
+ contact = :contact,
+ phone = :phone,
+ wechat = :wechat,
+ address = :address,
+ lat = :lat,
+ lng = :lng,
+ cover = :cover
+ WHERE id = :id");
+
+ $fixedPostCount = 0;
+ foreach ($posts as $p) {
+ $pid = $p['id'];
+ $needUpdate = false;
+
+ $contact = $p['contact'];
+ $phone = $p['phone'];
+ $wechat = $p['wechat'];
+ $address = $p['address'];
+ $lat = $p['lat'];
+ $lng = $p['lng'];
+ $cover = $p['cover'];
+
+ // 联系人缺失时按昵称或角色填充
+ if (empty($contact) || $contact === '测试') {
+ $contact = !empty($p['nickname']) ? $p['nickname'] : '同城房主';
+ $needUpdate = true;
+ }
+
+ // 电话缺失时用用户手机号
+ if (empty($phone) || $phone === '13000000000') {
+ $phone = !empty($p['mobile']) ? $p['mobile'] : '13885108888';
+ $needUpdate = true;
+ }
+
+ // 微信缺失时填充微信号
+ if (empty($wechat)) {
+ $wechat = 'gyfc' . $pid;
+ $needUpdate = true;
+ }
+
+ // 地址缺失时填充所在区域代表地址
+ $areaKey = !empty($p['area']) ? $p['area'] : '云岩区';
+ $def = isset($areaDefaultCoord[$areaKey]) ? $areaDefaultCoord[$areaKey] : $areaDefaultCoord['云岩区'];
+
+ if (empty($address)) {
+ $address = $def[2];
+ $needUpdate = true;
+ }
+
+ // 坐标为空时补充默认坐标并带有少许偏移
+ if (empty($lat) || empty($lng) || (float)$lat == 0.0) {
+ $offset = (($pid % 7) - 3) * 0.004;
+ $lat = round($def[0] + $offset, 6);
+ $lng = round($def[1] + $offset, 6);
+ $needUpdate = true;
+ }
+
+ // 封面缺失时兜底
+ if (empty($cover)) {
+ $cover = '/static/posts/job-warehouse.jpg';
+ $needUpdate = true;
+ }
+
+ if ($needUpdate) {
+ $stmtFixPost->execute([
+ ':contact' => $contact,
+ ':phone' => $phone,
+ ':wechat' => $wechat,
+ ':address' => $address,
+ ':lat' => $lat,
+ ':lng' => $lng,
+ ':cover' => $cover,
+ ':id' => $pid,
+ ]);
+ $fixedPostCount++;
+ }
+ }
+ echo "已检查并补全 {$fixedPostCount} 条帖子的联系信息、地址与坐标。" . PHP_EOL;
+
+ echo PHP_EOL . "==================================================" . PHP_EOL;
+ echo "步骤 5: 同步商家发帖计数与分类信息计数" . PHP_EOL;
+ echo "==================================================" . PHP_EOL;
+
+ // 1. 同步商家 post_count
+ $merchants = $pdo->query("SELECT id, user_id FROM fa_fc_merchant")->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($merchants as $m) {
+ $c = (int)$pdo->query("SELECT COUNT(*) FROM fa_fc_post WHERE user_id = {$m['user_id']} AND status = 1 AND audit_status = 1 AND deletetime IS NULL")->fetchColumn();
+ $pdo->query("UPDATE fa_fc_merchant SET post_count = {$c} WHERE id = {$m['id']}");
+ echo "商家 ID {$m['id']} (user_id={$m['user_id']}) 在招/在售有效信息量: {$c}" . PHP_EOL;
+ }
+
+ // 2. 同步分类 post_count
+ for ($cid = 1; $cid <= 8; $cid++) {
+ $cc = (int)$pdo->query("SELECT COUNT(*) FROM fa_fc_post WHERE category_id = {$cid} AND status = 1 AND audit_status = 1 AND deletetime IS NULL")->fetchColumn();
+ $pdo->query("UPDATE fa_fc_category SET post_count = {$cc} WHERE id = {$cid}");
+ }
+ echo "已成功同步所有 8 个主分类的有效发帖总数。" . PHP_EOL;
+
+ echo PHP_EOL . "==================================================" . PHP_EOL;
+ echo "数据库测试数据修复完毕!" . PHP_EOL;
+ echo "==================================================" . PHP_EOL;
+}
+
+repairDatabaseData();
diff --git a/tools/verify_api_data.php b/tools/verify_api_data.php
new file mode 100644
index 0000000..e7ea5a2
--- /dev/null
+++ b/tools/verify_api_data.php
@@ -0,0 +1,61 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ echo "=== 1. 验证商家帖子详情 (如 ID: 101 - 顺捷同城物流园) ===" . PHP_EOL;
+ $post101 = $pdo->query("SELECT * FROM fa_fc_post WHERE id = 101")->fetch(PDO::FETCH_ASSOC);
+ $user101 = $pdo->query("SELECT * FROM fa_user WHERE id = {$post101['user_id']}")->fetch(PDO::FETCH_ASSOC);
+ $merchant101 = $pdo->query("SELECT * FROM fa_fc_merchant WHERE user_id = {$post101['user_id']}")->fetch(PDO::FETCH_ASSOC);
+
+ echo "标题: {$post101['title']}" . PHP_EOL;
+ echo "identity_type: {$post101['identity_type']} (应为2)" . PHP_EOL;
+ echo "发布人: {$user101['nickname']}, 头像: {$user101['avatar']}" . PHP_EOL;
+ echo "关联商家: {$merchant101['name']}" . PHP_EOL;
+ echo " - logo: {$merchant101['logo']}" . PHP_EOL;
+ echo " - banner: {$merchant101['banner']}" . PHP_EOL;
+ echo " - address: {$merchant101['address']}" . PHP_EOL;
+ echo " - contact: {$merchant101['contact']}" . PHP_EOL;
+ echo " - category: {$merchant101['category']}" . PHP_EOL;
+ echo " - notice: {$merchant101['notice']}" . PHP_EOL;
+ echo " - post_count: {$merchant101['post_count']}" . PHP_EOL;
+
+ echo PHP_EOL . "=== 2. 验证个人帖子详情 (如 ID: 102 - 小王同学) ===" . PHP_EOL;
+ $post102 = $pdo->query("SELECT * FROM fa_fc_post WHERE id = 102")->fetch(PDO::FETCH_ASSOC);
+ $user102 = $pdo->query("SELECT * FROM fa_user WHERE id = {$post102['user_id']}")->fetch(PDO::FETCH_ASSOC);
+ echo "标题: {$post102['title']}" . PHP_EOL;
+ echo "identity_type: {$post102['identity_type']} (应为1)" . PHP_EOL;
+ echo "发布人: {$user102['nickname']}, 头像: {$user102['avatar']}" . PHP_EOL;
+ echo "联系人: {$post102['contact']}, 手机: {$post102['phone']}, 微信: {$post102['wechat']}" . PHP_EOL;
+
+ echo PHP_EOL . "=== 3. 验证所有商家的档案完整性 ===" . PHP_EOL;
+ $merchants = $pdo->query("SELECT * FROM fa_fc_merchant")->fetchAll(PDO::FETCH_ASSOC);
+ $allComplete = true;
+ foreach ($merchants as $m) {
+ $missing = [];
+ if (empty($m['logo'])) $missing[] = 'logo';
+ if (empty($m['banner'])) $missing[] = 'banner';
+ if (empty($m['address'])) $missing[] = 'address';
+ if (empty($m['lat']) || empty($m['lng'])) $missing[] = 'lat/lng';
+ if (empty($m['contact'])) $missing[] = 'contact';
+ if (empty($m['category'])) $missing[] = 'category';
+ if (empty($m['notice'])) $missing[] = 'notice';
+
+ if (!empty($missing)) {
+ echo "商家 {$m['name']} 缺失字段: " . implode(', ', $missing) . PHP_EOL;
+ $allComplete = false;
+ } else {
+ echo "✓ 商家 [{$m['id']}] {$m['name']} 数据完整 (有效发帖: {$m['post_count']})" . PHP_EOL;
+ }
+ }
+ if ($allComplete) {
+ echo ">>> 全部商家数据 100% 完整!<<<" . PHP_EOL;
+ }
+}
+
+verifyApiOutputs();
diff --git a/uniapp/App.vue b/uniapp/App.vue
index b7b8145..33371d9 100644
--- a/uniapp/App.vue
+++ b/uniapp/App.vue
@@ -53,8 +53,10 @@ view,
text,
scroll-view,
swiper,
-image {
+image,
+button {
box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
}
/* 通用容器 */
@@ -129,6 +131,7 @@ image {
}
.fc-safe-bottom {
+ height: constant(safe-area-inset-bottom);
height: env(safe-area-inset-bottom);
}
diff --git a/uniapp/common/api/message.js b/uniapp/common/api/message.js
index 87945b0..ea90304 100644
--- a/uniapp/common/api/message.js
+++ b/uniapp/common/api/message.js
@@ -5,12 +5,46 @@
import { get, post } from '../utils/request.js'
export const messageApi = {
- /** 消息列表(type: system / interact) */
+ /**
+ * 消息列表
+ * @param {Object} params { type?: string, page?: number, size?: number }
+ * @returns {Promise