文件
fenleixinxi/application/common/library/CouponService.php
MiAn cb12bf5fc8 feat: 新增地址选择插件及订阅消息等多项功能优化
- 新增地址选择插件,支持地图位置选择并返回地址和经纬度
- 新增微信订阅消息发送功能,支持审核结果、提现通知等场景
- 新增订单支付状态查询、提现收款人姓名保存等接口
- 新增服务区域管理,支持按省市区划分并同步到信息发布
- 新增资金流水和营收报表导出功能
- 新增可用优惠券查询、订单退款拆分处理等功能
- 优化后台角标显示、首页数据统计、UI配置下发等
- 修复订单支付并发问题、地区管理逻辑等
2026-10-22 17:14:56 +08:00

179 行
5.3 KiB
PHP

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

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

<?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);
}
}