文件
fenleixinxi/application/api/controller/Order.php

323 行
13 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use app\common\library\CouponService;
use app\common\library\OrderClose;
use think\Db;
/**
* 用户端 · 订单(Module-ORDER)
* 路由:
* POST /api/v1/order/recharge 余额充值下单
*
* 说明:充值金额一律由客户端传入后**服务端再次校验**(>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);
}
}