where('enabled', 1) ->order('sort asc') ->select(); $list = []; foreach ($rows as $row) { $list[] = [ 'planId' => (string)$row['plan_code'], 'name' => (string)$row['name'], 'price' => (float)$row['price'], 'originPrice' => (float)$row['origin_price'], 'days' => (int)$row['days'], 'tag' => (string)$row['tag'], // 套餐含发布条数(后台「发布与费用配置」同步;0 表示不含发布额度) 'postQuota' => isset($row['post_quota']) ? (int)$row['post_quota'] : 0, ]; } $this->ok($list); } /** * M2 会员权益 * GET /api/v1/member/benefits → {freeView,postDiscount,distRate,benefits[]} * 规则:读配置 member.benefit,未配置返回默认 */ public function benefits() { $default = [ 'freeView' => 20, 'postDiscount' => '8 折', 'distRate' => '一级 20% / 二级 10%', 'benefits' => ['每日免费查看 20 条联系方式', '发布信息 8 折', '专属会员标识', '分销佣金加速结算'], ]; $cfg = $this->cfg('member.benefit', null); if (!is_array($cfg)) { $cfg = $default; } $benefits = isset($cfg['benefits']) && is_array($cfg['benefits']) && $cfg['benefits'] ? array_values($cfg['benefits']) : $default['benefits']; $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, ]); } /** * M3 开通会员 * POST /api/v1/member/open {planId,payType} * → {orderNo,status,order,expireAt,postQuota,quotaLeft,quotaExpireAt} * - balance:余额支付,下单即扣款并**立即发放会员**(status=paid,order=null) * - wxpay :订单 pending(order={orderNo,amount,payType,status:'pending'}), * 前端拿 orderNo 调 POST /pay/prepay 调起收银台,支付成功后由 /pay/notify 发放会员 * 规则 BR-07;套餐按 plan_code 查;金额取 fc_plan.price(服务端计算,忽略客户端传值) */ public function open() { $user = $this->auth(); $planId = trim((string)$this->input('planId', '')); if ($planId === '') { $this->fail('请选择会员套餐'); } $payType = (string)$this->input('payType', ''); if (!in_array($payType, ['wxpay', 'balance'], true)) { $this->fail('支付方式不支持'); } $plan = Db::name('fc_plan')->where('plan_code', $planId)->find(); if (!$plan) { $this->fail('套餐不存在'); } $amount = round((float)$plan['price'], 2); $now = time(); $isBalance = $payType === 'balance'; // 余额校验必须在落订单之前:否则余额不足时已插入一条 status=paid 的假订单 $balance = (float)$user['money']; if ($isBalance && $balance < $amount) { $this->fail('余额不足'); } $orderNo = $this->orderNo('MO'); $orderId = Db::name('fc_order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $user['id'], 'type' => 'member', 'title' => (string)$plan['name'], 'amount' => $amount, 'pay_type' => $payType, 'status' => $isBalance ? 'paid' : 'pending', 'paytime' => $isBalance ? $now : null, // related_id 存套餐主键:支付回调 /pay/notify 据此复用 grantMembership(),回调可独立完成 'related_id' => (int)$plan['id'], 'createtime' => $now, 'updatetime' => $now, ]); if ($isBalance) { $balance = round($balance - $amount, 2); Db::name('user')->where('id', $user['id'])->update([ 'money' => $balance, 'updatetime' => $now, ]); // 余额支付:立即发放会员(微信支付等待回调发放) $grant = $this->grantMembership(Db::name('fc_order')->where('id', $orderId)->find()); } else { $grant = null; } $quota = isset($plan['post_quota']) ? max(0, (int)$plan['post_quota']) : 0; $this->ok([ 'orderNo' => $orderNo, 'status' => $isBalance ? 'paid' : 'pending', // 微信支付:前端拿到 orderNo 后调 /pay/prepay;余额支付无需再支付(order=null) 'order' => $isBalance ? null : [ 'orderNo' => $orderNo, 'amount' => $amount, 'payType' => 'wxpay', 'status' => 'pending', ], 'expireAt' => $grant ? $this->iso($grant['expireAt']) : null, 'postQuota' => $quota, 'quotaLeft' => $grant ? $grant['quotaLeft'] : $this->quotaLeft($this->ext($user['id'])), 'quotaExpireAt' => $grant ? $this->iso($grant['expireAt']) : null, ]); } /** * 会员发放(**可复用**:余额支付即时发放 / 微信支付回调 /pay/notify 发放) * 说明:以 fc_order.related_id(套餐主键)反查 fc_plan 取天数与赠送发布条数; * 老订单(related_id=0)按订单标题(套餐名)反查,仍失败则按默认 30 天、0 条额度发放; * 发放同时记「会员开通」资金流水并发站内信。 * @param array $order fc_order 行(需含 user_id / related_id / amount / id / title) * @return array{expireAt:int,postQuota:int,quotaLeft:int} */ public function grantMembership(array $order) { $userId = (int)$order['user_id']; $now = time(); $plan = null; $planId = (int)(isset($order['related_id']) ? $order['related_id'] : 0); if ($planId > 0) { $plan = Db::name('fc_plan')->where('id', $planId)->find(); } if (!$plan) { $title = isset($order['title']) ? (string)$order['title'] : ''; if ($title !== '') { $plan = Db::name('fc_plan')->where('name', $title)->find(); } } $days = $plan ? max(0, (int)$plan['days']) : 30; $quota = $plan && isset($plan['post_quota']) ? max(0, (int)$plan['post_quota']) : 0; $level = $plan ? (string)$plan['name'] : '会员'; $ext = $this->ext($userId); $base = (int)$ext['member_expiretime']; if ($base < $now) { $base = $now; } $expire = $base + $days * 86400; // 发布套餐额度:月/年卡赠送「含发布条数」,额度随会员有效期(过期后由发布接口自动清零) $left = (int)(isset($ext['post_quota_left']) ? $ext['post_quota_left'] : 0); $quotaExpire = (int)(isset($ext['post_quota_expire']) ? $ext['post_quota_expire'] : 0); if ($quotaExpire > 0 && $quotaExpire < $now) { $left = 0; // 上一周期额度已过期,不再累计 } $left += $quota; Db::name('fc_user_ext')->where('user_id', $userId)->update([ 'is_member' => 1, 'member_level' => $level, 'member_expiretime' => $expire, 'post_quota_left' => $left, 'post_quota_expire' => $expire, 'updatetime' => $now, ]); $amount = round((float)(isset($order['amount']) ? $order['amount'] : 0), 2); $orderId = (int)(isset($order['id']) ? $order['id'] : 0); $this->flow('income', '会员开通', $amount, 0, $orderId); $this->notify( $userId, 'system', '会员开通成功', '您已开通「' . $level . '」,有效期至 ' . date('Y-m-d', $expire) . ',赠送发布条数 ' . $quota . ' 条。' ); return ['expireAt' => $expire, 'postQuota' => $quota, 'quotaLeft' => $left]; } /** * 剩余发布条数(供未开通时回显) * @param array $ext * @return int */ protected function quotaLeft($ext) { $left = (int)(isset($ext['post_quota_left']) ? $ext['post_quota_left'] : 0); $expire = (int)(isset($ext['post_quota_expire']) ? $ext['post_quota_expire'] : 0); if ($left > 0 && $expire > 0 && $expire < time()) { return 0; } return $left; } /** * M4 我的订单 * GET /api/v1/member/orders {page,size} → {list,page,size,total,hasMore} * 规则:按 createtime desc */ public function orders() { $user = $this->auth(); list($page, $size) = $this->pager(); $total = Db::name('fc_order')->where('user_id', $user['id'])->count(); $rows = Db::name('fc_order') ->where('user_id', $user['id']) ->order('createtime desc') ->page($page, $size) ->select(); $list = []; foreach ($rows as $row) { $list[] = [ 'id' => (int)$row['id'], 'orderNo' => (string)$row['order_no'], 'type' => (string)$row['type'], 'title' => (string)$row['title'], 'amount' => (float)$row['amount'], 'payType' => (string)$row['pay_type'], 'status' => (string)$row['status'], 'createdAt' => $this->iso($row['createtime']), ]; } $this->ok($this->paginate($list, $total, $page, $size)); } /** * M5 我的资产 * GET /api/v1/member/assets * → {balance,coupons:[{id,couponId,name,type,amount,minAmount,status,expireAt}],couponCount,points} * 说明:coupons 来自 fc_user_coupon LEFT JOIN fc_coupon(真实数据); * status:unused 未使用 / used 已使用 / expired 已过期(有效期为 0 表示永久; * status=unused 但已过期时**动态判定**为 expired,无需定时任务)。 */ public function assets() { $user = $this->auth(); $now = time(); $rows = Db::name('fc_user_coupon')->alias('uc') ->join('fc_coupon c', 'c.id = uc.coupon_id', 'LEFT') ->where('uc.user_id', $user['id']) ->field('uc.id,uc.coupon_id,uc.status,uc.expiretime,uc.createtime,c.name,c.type,c.amount,c.min_amount') ->order('uc.id desc') ->select(); $coupons = []; foreach ($rows as $row) { $status = (string)$row['status']; $expire = !empty($row['expiretime']) ? (int)$row['expiretime'] : 0; if ($status === 'unused' && $expire > 0 && $expire < $now) { $status = 'expired'; } $coupons[] = [ 'id' => (int)$row['id'], 'couponId' => (int)$row['coupon_id'], 'name' => isset($row['name']) ? (string)$row['name'] : '', 'type' => isset($row['type']) ? (string)$row['type'] : '', 'amount' => isset($row['amount']) ? (float)$row['amount'] : 0, 'minAmount' => isset($row['min_amount']) ? (float)$row['min_amount'] : 0, 'status' => $status, 'expireAt' => $expire > 0 ? $this->iso($expire) : null, ]; } $this->ok([ 'balance' => (float)$user['money'], 'coupons' => $coupons, 'couponCount' => count($coupons), 'points' => (int)$user['score'], ]); } }