323 行
13 KiB
PHP
323 行
13 KiB
PHP
<?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);
|
||
}
|
||
}
|