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} 分页列表数据 + */ list: (params = {}) => get('/message/list', params, { silent: true }), - /** 未读消息数(首页角标) → { count } */ + + /** + * 未读消息数(首页角标) + * @returns {Promise} { count: number } + */ unreadCount: () => get('/message/unread-count', {}, { silent: true }), - /** 标记已读 */ + + /** + * 标记单条已读 + * @param {number|string} id 消息ID + * @returns {Promise} { read: boolean } + */ read: (id) => post(`/message/${id}/read`, {}, { silent: true }), + + /** + * 一键全部已读 + * @param {Object} params { type?: string } + * @returns {Promise} { success: boolean, affected: number } + */ + readAll: (params = {}) => post('/message/read-all', params), + + /** + * 一键清理消息 + * @param {Object} params { type?: string } + * @returns {Promise} { success: boolean, affected: number } + */ + clear: (params = {}) => post('/message/clear', params), + + /** + * 删除单条消息 + * @param {number|string} id 消息ID + * @returns {Promise} { success: boolean } + */ + delete: (id) => post(`/message/${id}/delete`, {}), } export default messageApi diff --git a/uniapp/common/assets/banners.js b/uniapp/common/assets/banners.js index 5e54158..4dd4cb4 100644 --- a/uniapp/common/assets/banners.js +++ b/uniapp/common/assets/banners.js @@ -14,9 +14,9 @@ export const BANNER_ART = ['banner-art-1.png', 'banner-art-2.png', 'banner-art-3 /** 内置生态绿渐变(1/2/3 号,取自 uni.scss 主色家族),后台未配置颜色时按位置取用 */ export const BANNER_BG = [ - 'linear-gradient(105deg, #00e070 0%, #00c860 42%, #046b3b 100%)', - 'linear-gradient(105deg, #00d96c 0%, #00b85c 40%, #023a21 100%)', - 'linear-gradient(105deg, #00e878 0%, #049e52 45%, #045c34 100%)', + 'linear-gradient(105deg, #00d26a 0%, #10b981 50%, #34d399 100%)', + 'linear-gradient(105deg, #10b981 0%, #34d399 50%, #6ee7b7 100%)', + 'linear-gradient(105deg, #059669 0%, #10b981 50%, #34d399 100%)', ] /** diff --git a/uniapp/common/mock/data.js b/uniapp/common/mock/data.js index 8fdd0e0..57ad40f 100644 --- a/uniapp/common/mock/data.js +++ b/uniapp/common/mock/data.js @@ -24,9 +24,9 @@ export const datasets = { /** 首页轮播广告位(color 为生态绿家族渐变,image 为空时前端用「绿底 + 白色插画」内置配图) */ banners: [ - { id: 1, title: '同城好物节 · 二手直降', image: '', link: '/pages/category/channel?categoryId=3', color: 'linear-gradient(105deg, #00e070 0%, #00c860 42%, #046b3b 100%)' }, - { id: 2, title: '招聘旺季 · 免费发布', image: '', link: '/pages/post/create', color: 'linear-gradient(105deg, #00d96c 0%, #00b85c 40%, #023a21 100%)' }, - { id: 3, title: '本地服务 · 上门更省心', image: '', link: '/pages/category/channel?categoryId=5', color: 'linear-gradient(105deg, #00e878 0%, #049e52 45%, #045c34 100%)' }, + { id: 1, title: '同城好物节 · 二手直降', image: '/static/banners/banner-1.png', link: '/pages/category/channel?categoryId=3', color: 'linear-gradient(105deg, #10b981 0%, #34d399 50%, #a7f3d0 100%)' }, + { id: 2, title: '招聘旺季 · 免费发布', image: '/static/banners/banner-2.png', link: '/pages/post/create', color: 'linear-gradient(105deg, #059669 0%, #10b981 50%, #6ee7b7 100%)' }, + { id: 3, title: '本地服务 · 上门更省心', image: '/static/banners/banner-3.png', link: '/pages/category/channel?categoryId=5', color: 'linear-gradient(105deg, #00d26a 0%, #34d399 50%, #bbf7d0 100%)' }, ], /** 分类导航 */ diff --git a/uniapp/components/fc-navbar/fc-navbar.vue b/uniapp/components/fc-navbar/fc-navbar.vue index fcb9919..3e9a7dc 100644 --- a/uniapp/components/fc-navbar/fc-navbar.vue +++ b/uniapp/components/fc-navbar/fc-navbar.vue @@ -77,10 +77,9 @@ export default { syncStatusBarStyle() { try { if (typeof uni !== 'undefined' && uni.setNavigationBarColor) { - const isLightBg = this.theme === 'white' || this.theme === 'transparent' uni.setNavigationBarColor({ - frontColor: isLightBg ? '#000000' : '#ffffff', - backgroundColor: isLightBg ? '#ffffff' : '#04874a', + frontColor: '#000000', + backgroundColor: this.theme === 'gradient' ? '#eafbf1' : '#ffffff', }) } } catch (e) {} @@ -152,10 +151,10 @@ export default { &--gradient { background: $fc-gradient-hero; .fc-navbar__title-text { - color: #ffffff; + color: $fc-text; } .fc-navbar__back-icon { - color: #ffffff; + color: $fc-text; } } &--white { diff --git a/uniapp/components/fc-post-card/fc-post-card.vue b/uniapp/components/fc-post-card/fc-post-card.vue index 85ee986..8508004 100644 --- a/uniapp/components/fc-post-card/fc-post-card.vue +++ b/uniapp/components/fc-post-card/fc-post-card.vue @@ -216,9 +216,12 @@ export default { flex-shrink: 0; } .fc-post-card__tag--top { - background: $fc-gradient-primary; - color: $fc-primary-ink; + background: linear-gradient(135deg, #00d26a 0%, #00bd5f 100%); + color: #ffffff; font-weight: 700; + border-radius: 6rpx; + padding: 2rpx 10rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 210, 106, 0.35); } .fc-post-card__meta { display: flex; diff --git a/uniapp/components/fc-tabbar/fc-tabbar.vue b/uniapp/components/fc-tabbar/fc-tabbar.vue index f544aad..68cc200 100644 --- a/uniapp/components/fc-tabbar/fc-tabbar.vue +++ b/uniapp/components/fc-tabbar/fc-tabbar.vue @@ -5,14 +5,41 @@ v-for="(item, idx) in tabs" :key="item.path" class="fc-tabbar__item" - :class="{ 'fc-tabbar__item--active': current === idx, 'fc-tabbar__item--primary': item.primary }" + :class="{ + 'fc-tabbar__item--active': current === idx, + 'fc-tabbar__item--primary': item.primary + }" + hover-class="none" @click="switchTo(item, idx)" > - - - - - {{ item.text }} + + + + + @@ -21,6 +48,8 @@