0 且 ≤100000),并按 round(...,2) 归一; * **交易只允许微信支付**:订单落 status=pending,前端调 POST /pay/prepay 调起收银台, * 支付成功后由 /pay/notify 复用 creditRecharge() 入账(幂等由回调侧 status=paid 保证)。 * * 【为什么不允许 payType=balance】「用余额充值余额」在本模型里没有任何收款方:余额只会 +amount、 * 不会扣减,等于给用户凭空增发余额(可用余额去买会员/付费发布/解锁联系方式,直接造成资损)。 * v1.1.0 初版曾把它当作「余额即时到账」的演示通道,**已移除**;给用户加余额只应来自 * ① 微信支付回调 creditRecharge(),② 后台「会员管理 → 余额充值」(`fc/userext/recharge`)。 */ class Order extends V1 { /** * POST /api/v1/order/recharge {amount,payType} * → {orderNo,amount,status:'pending',payType:'wxpay'} * 规则:金额 >0 且 ≤100000 → 否则 fail('充值金额不合法'); * payType=balance → fail('余额充值仅支持微信支付')(资损防护,见类注释); * payType 非 wxpay → fail('支付方式不支持')。 */ public function recharge() { $user = $this->auth(); $amount = round((float)$this->input('amount', 0), 2); if ($amount <= 0 || $amount > 100000) { $this->fail('充值金额不合法'); } $payType = (string)$this->input('payType', ''); // 余额「充值」= 用余额给余额加钱(无收款方、余额不扣减)→ 直接拒绝,防资损 if ($payType === 'balance') { $this->fail('余额充值仅支持微信支付'); } if ($payType !== 'wxpay') { $this->fail('支付方式不支持'); } $now = time(); $orderNo = $this->orderNo('RC'); Db::name('fc_order')->insert([ 'order_no' => $orderNo, 'user_id' => $user['id'], 'type' => 'recharge', 'title' => '余额充值', 'amount' => $amount, 'pay_type' => 'wxpay', 'status' => 'pending', 'paytime' => null, 'related_id' => 0, 'createtime' => $now, 'updatetime' => $now, ]); // 入账只在「微信支付回调 /pay/notify」里发生(Order::creditRecharge),本接口不触碰 fa_user.money $this->ok([ 'orderNo' => $orderNo, 'amount' => $amount, 'status' => 'pending', 'payType' => 'wxpay', ]); } /** * 充值入账(**可复用**:微信支付回调 /pay/notify 入账时调用) * 动作:fa_user.money 加款 + fc_finance_flow 资金流水 + fc_message 站内信。 * 幂等:调用方需保证「订单未入账」(回调侧以 fc_order.status=paid 做幂等判定)。 * 注意:**不要**在充值下单接口里调用它 —— 未真实收款就加余额属于凭空增发(资损)。 * @param array $order fc_order 行 * @return array{balance:float,amount:float} */ public function creditRecharge(array $order) { $userId = (int)$order['user_id']; $amount = round((float)(isset($order['amount']) ? $order['amount'] : 0), 2); $orderId = (int)(isset($order['id']) ? $order['id'] : 0); $now = time(); $row = Db::name('user')->where('id', $userId)->find(); if (!$row) { return ['balance' => 0, 'amount' => $amount]; } // 原子自增:并发回调/重复入账时不会丢更新(幂等由订单状态保证) Db::name('user')->where('id', $userId)->setInc('money', $amount); $balance = round((float)Db::name('user')->where('id', $userId)->value('money'), 2); Db::name('user')->where('id', $userId)->update(['updatetime' => $now]); $this->flow('income', '余额充值', $amount, $balance, $orderId, '余额充值', $userId); // 钱包流水(fa_user.money 增)→ 余额中心「余额明细」 $this->walletFlow($userId, 'income', '余额充值', $amount, $balance, $orderId, '余额充值'); $this->notify( $userId, 'system', '充值成功', '您已成功充值 ' . number_format($amount, 2) . ' 元,当前余额 ' . number_format($balance, 2) . ' 元。' ); return ['balance' => $balance, 'amount' => $amount]; } /** * POST /api/v1/order/cancel * 主动取消未支付订单 * 入参:{orderNo} 或 {id} * * @return void */ public function cancel() { $user = $this->auth(); $orderNo = trim((string)$this->input('orderNo', '')); $orderId = (int)$this->input('id', 0); if ($orderNo === '' && $orderId <= 0) { $this->fail('请提供订单编号或订单ID'); } $query = Db::name('fc_order')->where('user_id', $user['id']); if ($orderNo !== '') { $query->where('order_no', $orderNo); } else { $query->where('id', $orderId); } $order = $query->find(); if (!$order) { $this->fail('订单不存在', 404); } if ((string)$order['status'] === 'paid') { $this->fail('已支付订单无法取消,如需退款请联系客服'); } if ((string)$order['status'] === 'closed') { $this->fail('订单已是关闭状态,无需重复取消'); } if ((string)$order['status'] !== 'pending') { $this->fail('当前订单状态不支持取消'); } $closed = OrderClose::close($order, 'user_cancel'); if (!$closed) { $this->fail('取消订单失败,请稍后重试'); } $this->ok([ 'orderNo' => (string)$order['order_no'], 'status' => 'closed', ], '订单已成功取消'); } /** * GET /api/v1/order/detail * 获取订单详情 * 入参:orderNo 或 id * * @return void */ public function detail() { $user = $this->auth(); $orderNo = trim((string)$this->input('orderNo', '')); $orderId = (int)$this->input('id', 0); if ($orderNo === '' && $orderId <= 0) { $this->fail('请提供订单编号或订单ID'); } $query = Db::name('fc_order')->where('user_id', $user['id']); if ($orderNo !== '') { $query->where('order_no', $orderNo); } else { $query->where('id', $orderId); } $order = $query->find(); if (!$order) { $this->fail('订单不存在', 404); } // 惰性超时检查:如果是待支付且已超时,自动关闭 $order = OrderClose::checkAndCloseIfExpired($order); $now = time(); $timeoutSeconds = OrderClose::getTimeoutSeconds(); $createtime = isset($order['createtime']) ? (int)$order['createtime'] : 0; $expireSeconds = 0; if ((string)$order['status'] === 'pending' && $createtime > 0) { $expireSeconds = max(0, ($createtime + $timeoutSeconds) - $now); } // 状态与类型文本映射 $statusMap = [ 'pending' => '待支付', 'paid' => '已支付', 'refunding' => '退款中', 'refunded' => '已退款', 'closed' => '已关闭', ]; $typeMap = [ 'post' => '信息发布', 'member' => '开通会员', 'recharge' => '余额充值', 'payview' => '付费查看', ]; $type = (string)$order['type']; $relatedId = (int)$order['related_id']; $relatedInfo = null; // 根据业务类型获取详细关联信息 if ($type === 'post' && $relatedId > 0) { $post = Db::name('fc_post')->where('id', $relatedId)->find(); if ($post) { $categoryName = Db::name('fc_category')->where('id', (int)$post['category_id'])->value('name') ?: ''; $relatedInfo = [ 'postId' => (int)$post['id'], 'title' => (string)$post['title'], 'categoryName' => $categoryName, 'cover' => (string)$post['cover'], 'price' => (float)$post['price'], 'priceUnit' => (string)$post['price_unit'], 'auditStatus' => (int)$post['audit_status'], 'status' => (int)$post['status'], ]; } } elseif ($type === 'member' && $relatedId > 0) { $plan = Db::name('fc_plan')->where('id', $relatedId)->find(); if ($plan) { $postQuota = (int)$plan['post_quota']; $rights = ['专属会员身份标示']; if ($postQuota > 0) { $rights[] = "赠送免费发布 {$postQuota} 条"; } $rights[] = '同城专属推广推荐'; $rights[] = '信息置顶尊享特惠'; $relatedInfo = [ 'planId' => (int)$plan['id'], 'name' => (string)$plan['name'], 'days' => (int)$plan['days'], 'originPrice' => (float)$plan['origin_price'], 'price' => (float)$plan['price'], 'postQuota' => $postQuota, 'rights' => $rights, ]; } } elseif ($type === 'payview' && $relatedId > 0) { $post = Db::name('fc_post')->where('id', $relatedId)->find(); if ($post) { $categoryName = Db::name('fc_category')->where('id', (int)$post['category_id'])->value('name') ?: ''; $relatedInfo = [ 'postId' => (int)$post['id'], 'title' => (string)$post['title'], 'categoryName' => $categoryName, 'contact' => (string)$post['contact'], 'phone' => (string)$post['phone'], ]; } } elseif ($type === 'recharge') { $relatedInfo = [ 'target' => '同城便民服务钱包', 'usage' => '可用于全平台信息发布、置顶推广及各类便民生活增值服务', ]; } // 优惠券信息 $couponName = ''; if (!empty($order['coupon_id'])) { $couponName = (string)Db::name('fc_user_coupon')->alias('uc') ->join('fc_coupon c', 'c.id = uc.coupon_id', 'LEFT') ->where('uc.id', (int)$order['coupon_id']) ->value('c.name'); } // 用户与安全信息 $userMobile = isset($user['mobile']) ? (string)$user['mobile'] : ''; if ($userMobile !== '' && strlen($userMobile) >= 11) { $userMobile = substr($userMobile, 0, 3) . '****' . substr($userMobile, 7); } $detail = [ 'id' => (int)$order['id'], 'orderNo' => (string)$order['order_no'], 'type' => $type, 'typeText' => isset($typeMap[$type]) ? $typeMap[$type] : '其它订单', 'title' => (string)$order['title'], 'amount' => (float)$order['amount'], 'originAmount' => (!empty($order['origin_amount']) && (float)$order['origin_amount'] > 0) ? (float)$order['origin_amount'] : (float)$order['amount'], 'couponAmount' => isset($order['coupon_amount']) ? (float)$order['coupon_amount'] : 0.0, 'couponId' => isset($order['coupon_id']) ? (int)$order['coupon_id'] : 0, 'couponName' => $couponName, 'payType' => (string)$order['pay_type'], 'payTypeText' => (string)$order['pay_type'] === 'balance' ? '余额支付' : '微信支付', 'status' => (string)$order['status'], 'statusText' => isset($statusMap[$order['status']]) ? $statusMap[$order['status']] : (string)$order['status'], 'refundReason' => isset($order['refund_reason']) ? (string)$order['refund_reason'] : '', 'userAccount' => $userMobile ?: (isset($user['username']) ? (string)$user['username'] : ''), 'createdAt' => $this->iso($order['createtime']), 'payTime' => !empty($order['paytime']) ? $this->iso($order['paytime']) : null, 'closeTime' => ((string)$order['status'] === 'closed' && !empty($order['updatetime'])) ? $this->iso($order['updatetime']) : null, 'expireSeconds' => $expireSeconds, 'timeoutSeconds' => $timeoutSeconds, 'relatedId' => $relatedId, 'relatedInfo' => $relatedInfo, ]; $this->ok($detail); } }