- 新增地址选择插件,支持地图位置选择并返回地址和经纬度 - 新增微信订阅消息发送功能,支持审核结果、提现通知等场景 - 新增订单支付状态查询、提现收款人姓名保存等接口 - 新增服务区域管理,支持按省市区划分并同步到信息发布 - 新增资金流水和营收报表导出功能 - 新增可用优惠券查询、订单退款拆分处理等功能 - 优化后台角标显示、首页数据统计、UI配置下发等 - 修复订单支付并发问题、地区管理逻辑等
179 行
5.3 KiB
PHP
179 行
5.3 KiB
PHP
<?php
|
||
|
||
namespace app\common\library;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 优惠券抵扣(下单锁定 → 支付成功核销 → 退款/关闭退回)
|
||
*
|
||
* 券类型(fc_coupon.type):
|
||
* fixed 满减券:amount = 抵扣金额(元)
|
||
* discount 折扣券:amount = 折扣数(如 8.50 表示 8.5 折)
|
||
*
|
||
* 状态(fc_user_coupon.status):
|
||
* unused 未使用 · locked 已锁定(下单未支付)· used 已使用 · expired 已过期
|
||
*
|
||
* 规则:
|
||
* - 支付金额最低 0.01 元(抵扣后不会为 0);
|
||
* - 满减券抵扣不超过订单金额 - 0.01;
|
||
* - 锁定时使用条件更新,避免同一张券被并发使用。
|
||
*/
|
||
class CouponService
|
||
{
|
||
/**
|
||
* 计算券对指定金额的抵扣
|
||
*
|
||
* @param int $userId
|
||
* @param int $userCouponId fc_user_coupon.id
|
||
* @param float $amount 优惠前金额(元)
|
||
* @return array{discount:float,payable:float,userCouponId:int,couponId:int,name:string}
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function preview($userId, $userCouponId, $amount)
|
||
{
|
||
$amount = round((float)$amount, 2);
|
||
$userCouponId = (int)$userCouponId;
|
||
if ($userCouponId <= 0) {
|
||
throw new \RuntimeException('请选择优惠券');
|
||
}
|
||
|
||
$uc = Db::name('fc_user_coupon')->where('id', $userCouponId)->where('user_id', (int)$userId)->find();
|
||
if (!$uc) {
|
||
throw new \RuntimeException('优惠券不存在');
|
||
}
|
||
if ((string)$uc['status'] !== 'unused') {
|
||
throw new \RuntimeException('优惠券已使用或已失效');
|
||
}
|
||
if (!empty($uc['expiretime']) && (int)$uc['expiretime'] < time()) {
|
||
throw new \RuntimeException('优惠券已过期');
|
||
}
|
||
|
||
$coupon = Db::name('fc_coupon')->where('id', (int)$uc['coupon_id'])->find();
|
||
if (!$coupon || (int)$coupon['enabled'] !== 1) {
|
||
throw new \RuntimeException('优惠券已停用');
|
||
}
|
||
|
||
$min = round((float)$coupon['min_amount'], 2);
|
||
if ($min > 0 && $amount < $min) {
|
||
throw new \RuntimeException('订单金额未满 ' . $min . ' 元,无法使用该券');
|
||
}
|
||
|
||
$discount = self::calcDiscount($coupon, $amount);
|
||
if ($discount <= 0) {
|
||
throw new \RuntimeException('该优惠券不可用');
|
||
}
|
||
|
||
return [
|
||
'discount' => $discount,
|
||
'payable' => round($amount - $discount, 2),
|
||
'userCouponId' => $userCouponId,
|
||
'couponId' => (int)$coupon['id'],
|
||
'name' => (string)$coupon['name'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 下单锁券(必须在创建订单的同一事务内调用)
|
||
*
|
||
* @param int $userId
|
||
* @param int $userCouponId
|
||
* @param float $amount
|
||
* @param string $orderNo
|
||
* @return array 同 preview()
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function lock($userId, $userCouponId, $amount, $orderNo)
|
||
{
|
||
$info = self::preview($userId, $userCouponId, $amount);
|
||
|
||
$affected = Db::name('fc_user_coupon')
|
||
->where('id', (int)$userCouponId)
|
||
->where('user_id', (int)$userId)
|
||
->where('status', 'unused')
|
||
->update([
|
||
'status' => 'locked',
|
||
'order_no' => (string)$orderNo,
|
||
]);
|
||
if (!$affected) {
|
||
throw new \RuntimeException('优惠券已被使用,请重新选择');
|
||
}
|
||
|
||
return $info;
|
||
}
|
||
|
||
/**
|
||
* 支付成功核销(幂等:只处理 locked)
|
||
*
|
||
* @param string $orderNo
|
||
* @return void
|
||
*/
|
||
public static function markUsed($orderNo)
|
||
{
|
||
if ((string)$orderNo === '') {
|
||
return;
|
||
}
|
||
Db::name('fc_user_coupon')
|
||
->where('order_no', (string)$orderNo)
|
||
->where('status', 'locked')
|
||
->update([
|
||
'status' => 'used',
|
||
'usetime' => time(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 退回优惠券(订单关闭 / 退款)
|
||
*
|
||
* @param string $orderNo
|
||
* @return void
|
||
*/
|
||
public static function release($orderNo)
|
||
{
|
||
if ((string)$orderNo === '') {
|
||
return;
|
||
}
|
||
Db::name('fc_user_coupon')
|
||
->where('order_no', (string)$orderNo)
|
||
->where('status', 'in', ['locked', 'used'])
|
||
->update([
|
||
'status' => 'unused',
|
||
'order_no' => '',
|
||
'usetime' => null,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 抵扣金额计算
|
||
*
|
||
* @param array $coupon
|
||
* @param float $amount
|
||
* @return float
|
||
*/
|
||
protected static function calcDiscount(array $coupon, $amount)
|
||
{
|
||
$type = (string)$coupon['type'];
|
||
$value = round((float)$coupon['amount'], 2);
|
||
|
||
if ($type === 'discount') {
|
||
// amount = 折扣数(8.5 表示 8.5 折),折扣率 = amount / 10
|
||
$rate = $value / 10;
|
||
if ($rate <= 0 || $rate >= 1) {
|
||
return 0;
|
||
}
|
||
$discount = round($amount * (1 - $rate), 2);
|
||
} else {
|
||
// 满减
|
||
$discount = $value;
|
||
}
|
||
|
||
// 抵扣后至少支付 0.01 元
|
||
$maxDiscount = round($amount - 0.01, 2);
|
||
if ($discount > $maxDiscount) {
|
||
$discount = max(0, $maxDiscount);
|
||
}
|
||
|
||
return round($discount, 2);
|
||
}
|
||
}
|