auth(); $categoryId = (int)$this->input('categoryId', 0); $title = trim((string)$this->input('title', '')); $content = trim((string)$this->input('content', '')); if ($categoryId <= 0) { $this->fail('分类不能为空'); } if ($title === '') { $this->fail('标题不能为空'); } if ($content === '') { $this->fail('内容不能为空'); } if (mb_strlen($title) > 40) { $this->fail('标题不能超过 40 字'); } if (mb_strlen($content) > 500) { $this->fail('内容不能超过 500 字'); } $phone = trim((string)$this->input('phone', '')); if ($phone !== '' && !preg_match('/^1\d{10}$/', $phone)) { $this->fail('手机号格式不正确'); } $paid = (bool)$this->input('paid', false); $payType = (string)$this->input('payType', ''); $amount = 0; if ($paid) { $amount = $this->input('amount', null); if ($amount === null || $amount === '') { $amount = $this->cfg('fee.singlePost', 5); } $amount = round((float)$amount, 2); } $now = time(); $postId = Db::name('fc_post')->insertGetId([ 'user_id' => $user['id'], 'category_id' => $categoryId, 'title' => $title, 'content' => $content, 'cover' => '', 'media' => json_encode($this->mediaArray(), JSON_UNESCAPED_UNICODE), 'price' => round((float)$this->input('price', 0), 2), 'price_unit' => (string)$this->input('priceUnit', ''), 'area' => (string)$this->input('area', ''), 'address' => (string)$this->input('address', ''), 'contact' => (string)$this->input('contact', ''), 'phone' => $phone, 'wechat' => '', 'tags' => $this->tagsString(), 'is_paid' => $paid ? 1 : 0, 'amount' => $paid ? $amount : 0, 'pay_type' => $paid ? $payType : '', 'audit_status' => 0, 'views' => 0, 'likes' => 0, 'comment_count' => 0, 'status' => 1, 'createtime' => $now, 'updatetime' => $now, ]); $order = null; if ($paid) { $orderNo = $this->orderNo('PO'); Db::name('fc_order')->insert([ 'order_no' => $orderNo, 'user_id' => $user['id'], 'type' => 'post', 'title' => $title, 'amount' => $amount, 'pay_type' => $payType, 'status' => 'pending', 'related_id' => $postId, 'createtime' => $now, 'updatetime' => $now, ]); $order = ['orderNo' => $orderNo, 'amount' => $amount, 'payType' => $payType]; } $this->ok([ 'postId' => $postId, 'auditStatus' => 0, 'order' => $order, ]); } /** * P2 草稿列表 * GET /api/v1/post/drafts → [{draftId,title,content,categoryId,savedAt}] */ public function drafts() { $user = $this->auth(); $rows = Db::name('fc_draft') ->where('user_id', $user['id']) ->order('savedat desc') ->select(); $list = []; foreach ($rows as $row) { $list[] = [ 'draftId' => (int)$row['id'], 'title' => (string)$row['title'], 'content' => (string)$row['content'], 'categoryId' => (int)$row['category_id'], 'savedAt' => $this->iso($row['savedat']), ]; } $this->ok($list); } /** * P3 保存草稿 * POST /api/v1/post/draft {draftId?,categoryId?,title?,content?,media?,contact?,phone?,address?} * → {draftId,savedAt,categoryId,title,content} */ public function saveDraft() { $user = $this->auth(); $draftId = (int)$this->input('draftId', 0); $categoryId = (int)$this->input('categoryId', 0); $title = (string)$this->input('title', ''); $content = (string)$this->input('content', ''); $now = time(); $data = [ 'user_id' => $user['id'], 'category_id' => $categoryId, 'title' => $title, 'content' => $content, 'media' => json_encode($this->mediaArray(), JSON_UNESCAPED_UNICODE), 'contact' => (string)$this->input('contact', ''), 'phone' => (string)$this->input('phone', ''), 'address' => (string)$this->input('address', ''), 'savedat' => $now, 'updatetime' => $now, ]; if ($draftId > 0) { $exist = Db::name('fc_draft')->where('id', $draftId)->find(); if (!$exist || (int)$exist['user_id'] !== (int)$user['id']) { $this->fail('草稿不存在'); } Db::name('fc_draft')->where('id', $draftId)->update($data); } else { $data['createtime'] = $now; $draftId = Db::name('fc_draft')->insertGetId($data); } $this->ok([ 'draftId' => $draftId, 'savedAt' => $this->iso($now), 'categoryId' => $categoryId, 'title' => $title, 'content' => $content, ]); } /** * P4 删除信息(软删) * DELETE /api/v1/post/:id → {deleted:true} * 规则:仅能删除本人发布 */ public function remove() { $user = $this->auth(); $id = $this->routeId(); $post = Db::name('fc_post')->where('id', $id)->whereNull('deletetime')->find(); if (!$post) { $this->fail('信息不存在', 404); } if ((int)$post['user_id'] !== (int)$user['id']) { $this->fail('无权操作'); } Db::name('fc_post')->where('id', $id)->update([ 'deletetime' => time(), 'updatetime' => time(), ]); $this->ok(['deleted' => true]); } /** * P5 信息详情 * GET /api/v1/post/:id → Post 结构(contact 为脱敏串,绝不返回完整手机号) * 规则:允许游客;不存在 fail('信息不存在',404);每次浏览 views+1 */ public function detail() { // GET /post/:id(详情)与 DELETE /post/:id(软删)共用同一路径, // PHP 数组键无法为同一路径注册两条规则,故在此按请求方法分派。 if ($this->request->isDelete()) { $this->remove(); } $id = $this->routeId(); $post = Db::name('fc_post')->where('id', $id)->whereNull('deletetime')->find(); if (!$post) { $this->fail('信息不存在', 404); } Db::name('fc_post')->where('id', $id)->setInc('views'); $post['views'] = (int)$post['views'] + 1; $this->ok($this->buildPost($post)); } /** * P6 付费查看联系方式 * POST /api/v1/post/:id/pay-view {payType} * → {unlocked:true,contact:{phone,wechat}} * 规则 BR-05;金额取 fee.payView;会员或已解锁免费 */ public function payView() { $user = $this->auth(); $id = $this->routeId(); $payType = (string)$this->input('payType', ''); if (!in_array($payType, ['wxpay', 'balance'], true)) { $this->fail('支付方式不支持'); } $post = Db::name('fc_post')->where('id', $id)->whereNull('deletetime')->find(); if (!$post) { $this->fail('信息不存在', 404); } $contact = [ 'phone' => (string)$post['phone'], 'wechat' => $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $id), ]; // 已解锁直接返回 $exist = Db::name('fc_pay_view') ->where('user_id', $user['id']) ->where('post_id', $id) ->find(); if ($exist) { $this->ok(['unlocked' => true, 'contact' => $contact]); } $amount = round((float)$this->cfg('fee.payView', 2), 2); $now = time(); $ext = $this->ext($user['id']); $isMember = (int)$ext['is_member'] === 1 && (!$ext['member_expiretime'] || $ext['member_expiretime'] > $now); $orderId = 0; if ($isMember) { // 会员免费解锁 $amount = 0; $payType = 'free'; } elseif ($payType === 'balance') { $balance = (float)$user['money']; if ($balance < $amount) { $this->fail('余额不足'); } $balance = round($balance - $amount, 2); Db::name('user')->where('id', $user['id'])->update([ 'money' => $balance, 'updatetime' => $now, ]); $this->flow('expense', '付费查看', $amount, $balance, $id); } else { // 微信支付:创建已支付订单 $orderId = Db::name('fc_order')->insertGetId([ 'order_no' => $this->orderNo('VO'), 'user_id' => $user['id'], 'type' => 'post', 'title' => '付费查看 #' . $id, 'amount' => $amount, 'pay_type' => $payType, 'status' => 'paid', 'paytime' => $now, 'related_id' => $id, 'createtime' => $now, 'updatetime' => $now, ]); } Db::name('fc_pay_view')->insert([ 'user_id' => $user['id'], 'post_id' => $id, 'pay_type' => $payType, 'amount' => $amount, 'order_id' => $orderId, 'createtime' => $now, ]); $this->ok(['unlocked' => true, 'contact' => $contact]); } /** * P7 发表评价 * POST /api/v1/post/:id/comment {score,content} → {commentId} * 规则 BR-06 */ public function comment() { $user = $this->auth(); $id = $this->routeId(); $content = trim((string)$this->input('content', '')); if ($content === '') { $this->fail('评价内容不能为空'); } $score = (int)$this->input('score', 5); if ($score < 1 || $score > 5) { $this->fail('评分需在 1-5 之间'); } $post = Db::name('fc_post')->where('id', $id)->whereNull('deletetime')->find(); if (!$post) { $this->fail('信息不存在', 404); } $now = time(); $commentId = Db::name('fc_comment')->insertGetId([ 'post_id' => $id, 'user_id' => $user['id'], 'score' => $score, 'content' => $content, 'images' => '', 'reply' => '', 'top' => 0, 'status' => 1, 'createtime' => $now, 'updatetime' => $now, ]); Db::name('fc_post')->where('id', $id)->setInc('comment_count'); $this->notify( $post['user_id'], 'interact', '收到新评价', '用户「' . (string)$user['nickname'] . '」评价了您的信息:' . $content ); $this->ok(['commentId' => $commentId]); } /** * 读取路由 :id 参数(兼容 POST 体/查询串传入) * @return int */ protected function routeId() { $id = (int)$this->input('id', 0); if ($id <= 0) { $id = (int)$this->request->param('id', 0); } return $id; } /** * media 入参归一化为数组 * @return array */ protected function mediaArray() { $media = $this->input('media', []); if (is_string($media)) { $decoded = json_decode($media, true); $media = is_array($decoded) ? $decoded : []; } return is_array($media) ? array_values($media) : []; } /** * tags 入参归一化为逗号分隔字符串 * @return string */ protected function tagsString() { $tags = $this->input('tags', []); if (is_array($tags)) { $tags = implode(',', $tags); } return (string)$tags; } /** * 组装 Post 结构(与前端 mock 数据字段严格对齐) * @param array $row fc_post 行 * @return array */ protected function buildPost($row) { $categoryName = ''; if (!empty($row['category_id'])) { $cat = Db::name('fc_category')->where('id', $row['category_id'])->find(); if ($cat) { $categoryName = (string)$cat['name']; } } $publisher = ['id' => 0, 'name' => '', 'avatar' => '', 'isMerchant' => false, 'verified' => false]; if (!empty($row['user_id'])) { $pub = Db::name('user')->where('id', $row['user_id'])->find(); if ($pub) { $pubExt = Db::name('fc_user_ext')->where('user_id', $pub['id'])->find(); $isMerchant = $pubExt && (int)$pubExt['user_type'] === 2; $publisher = [ 'id' => (int)$pub['id'], 'name' => (string)$pub['nickname'], 'avatar' => (string)$pub['avatar'], 'isMerchant' => (bool)$isMerchant, 'verified' => (bool)$isMerchant, ]; } } $tags = []; if (!empty($row['tags'])) { foreach (explode(',', (string)$row['tags']) as $tag) { $tag = trim($tag); if ($tag !== '') { $tags[] = $tag; } } } return [ 'id' => (int)$row['id'], 'categoryId' => (int)$row['category_id'], 'categoryName' => $categoryName, 'title' => (string)$row['title'], 'content' => (string)$row['content'], 'cover' => (string)$row['cover'], 'price' => (float)$row['price'], 'priceUnit' => (string)$row['price_unit'], 'area' => (string)$row['area'], 'address' => (string)$row['address'], 'contact' => $this->maskPhone($row['phone']), 'views' => (int)$row['views'], 'likes' => (int)$row['likes'], 'isPaid' => (int)$row['is_paid'] === 1, 'auditStatus' => (int)$row['audit_status'], 'createdAt' => $this->iso($row['createtime']), 'tags' => $tags, 'publisher' => $publisher, 'commentCount' => (int)$row['comment_count'], 'distance' => (int)$row['distance'], ]; } }