PHP项目怎样实现用户积分抵扣?

wen PHP项目 18

本文目录导读:

PHP项目怎样实现用户积分抵扣?

  1. 数据库设计
  2. 核心逻辑实现
  3. 处理特殊情况
  4. 前端交互示例(Vue3)
  5. 最佳实践建议

在PHP项目中实现用户积分抵扣功能,通常涉及数据库设计积分计算逻辑订单处理事务控制,以下是完整实现方案:


数据库设计

用户表 users(积分字段)

ALTER TABLE users ADD COLUMN points INT DEFAULT 0 COMMENT '用户可用积分';

积分日志表 points_log(记录变动)

CREATE TABLE points_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    points INT NOT NULL COMMENT '正数增加,负数减少',
    type TINYINT NOT NULL COMMENT '1:获得 2:消费 3:过期 4:退款',
    source VARCHAR(50) COMMENT '来源:order_deduct/register/sign_in...',
    order_id INT DEFAULT NULL COMMENT '关联订单',
    remark VARCHAR(255),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

订单表 orders(记录抵扣)

ALTER TABLE orders ADD COLUMN points_deduct INT DEFAULT 0 COMMENT '抵扣积分数量';
ALTER TABLE orders ADD COLUMN points_deduct_amount DECIMAL(10,2) DEFAULT 0 COMMENT '抵扣金额';

核心逻辑实现

1 积分抵扣规则配置

// config/points.php
return [
    'points_rate' => 100,      // 100积分=1元
    'max_rate'    => 0.5,      // 最多抵扣订单金额50%
    'min_order'   => 10,       // 订单金额最低10元才能使用积分
    'max_points'  => 50000,    // 单次最多使用5万积分
];

2 积分抵扣计算服务

class PointDeductService
{
    private $config;
    public function __construct()
    {
        $this->config = config('points');
    }
    /**
     * 计算可抵扣金额
     */
    public function calculateDeduct(int $userId, int $orderAmount, int $pointsToUse): array
    {
        // 检查订单金额门槛
        if ($orderAmount < $this->config['min_order']) {
            return ['success' => false, 'msg' => '订单金额需满' . $this->config['min_order'] . '元'];
        }
        // 获取用户可用积分
        $userPoints = User::find($userId)->points;
        $pointsToUse = min($pointsToUse, $userPoints, $this->config['max_points']);
        // 计算可抵扣金额(向下取整,防止用户超用)
        $deductAmount = floor($pointsToUse / $this->config['points_rate']) * 100; // 单位分
        // 最多抵扣订单金额的50%
        $maxDeduct = floor($orderAmount * $this->config['max_rate']);
        $deductAmount = min($deductAmount, $maxDeduct);
        // 重新计算实际消耗积分
        $actualPoints = $deductAmount * $this->config['points_rate'];
        return [
            'success' => true,
            'deduct_amount' => $deductAmount,      // 抵扣金额(分)
            'deduct_points' => $actualPoints,       // 实际消耗积分
            'final_amount' => $orderAmount - $deductAmount // 最终应付
        ];
    }
    /**
     * 执行积分抵扣(与订单创建在同一事务中)
     */
    public function executeDeduct(int $userId, int $orderId, int $orderAmount, int $pointsToUse): array
    {
        DB::beginTransaction();
        try {
            // 1. 重新校验用户积分
            $user = User::lockForUpdate()->find($userId);
            if ($user->points < $pointsToUse) {
                throw new \Exception('积分不足');
            }
            // 2. 执行抵扣计算
            $result = $this->calculateDeduct($userId, $orderAmount, $pointsToUse);
            if (!$result['success']) {
                throw new \Exception($result['msg']);
            }
            // 3. 扣减用户积分
            $user->decrement('points', $result['deduct_points']);
            // 4. 记录积分消费日志
            PointsLog::create([
                'user_id' => $userId,
                'points' => -$result['deduct_points'],
                'type' => 2, // 消费
                'source' => 'order_deduct',
                'order_id' => $orderId,
                'remark' => "订单{$orderId}积分抵扣{$result['deduct_amount']}元"
            ]);
            // 5. 更新订单抵扣信息
            Order::where('id', $orderId)->update([
                'points_deduct' => $result['deduct_points'],
                'points_deduct_amount' => $result['deduct_amount'],
                'final_amount' => $result['final_amount']
            ]);
            DB::commit();
            return ['success' => true, 'data' => $result];
        } catch (\Exception $e) {
            DB::rollback();
            return ['success' => false, 'msg' => $e->getMessage()];
        }
    }
}

3 订单创建控制器示例

class OrderController extends Controller
{
    public function create(Request $request)
    {
        $orderAmount = 5000; // 订单金额5000分(50元)
        $userId = auth()->id();
        // 用户选择使用2000积分
        $usePoints = 2000;
        $service = new PointDeductService();
        // 1. 先校验和计算
        $calculResult = $service->calculateDeduct($userId, $orderAmount, $usePoints);
        if (!$calculResult['success']) {
            return response()->json(['error' => $calculResult['msg']], 400);
        }
        // 2. 创建订单(先扣库存等)
        $order = Order::create([
            'user_id' => $userId,
            'total_amount' => $orderAmount,
            'points_deduct' => 0, // 待更新
            'status' => 1
        ]);
        // 3. 执行积分抵扣(事务内)
        $deductResult = $service->executeDeduct($userId, $order->id, $orderAmount, $usePoints);
        if (!$deductResult['success']) {
            // 回滚订单创建(实际应使用全局事务)
            $order->delete();
            return response()->json(['error' => $deductResult['msg']], 400);
        }
        return response()->json(['order_id' => $order->id, 'final_amount' => $deductResult['data']['final_amount']]);
    }
}

处理特殊情况

1 订单退款退还积分

public function refundOrder(int $orderId)
{
    DB::transaction(function () use ($orderId) {
        $order = Order::findOrFail($orderId);
        if ($order->points_deduct > 0) {
            // 退还积分
            User::where('id', $order->user_id)
                ->increment('points', $order->points_deduct);
            // 记录退款日志
            PointsLog::create([
                'user_id' => $order->user_id,
                'points' => $order->points_deduct,
                'type' => 4, // 退款
                'source' => 'order_refund',
                'order_id' => $orderId,
                'remark' => "订单{$orderId}退款,退还积分{$order->points_deduct}"
            ]);
            // 清除订单抵扣记录
            $order->update(['points_deduct' => 0, 'points_deduct_amount' => 0]);
        }
    });
}

2 防止超用积分(并发安全)

// 使用悲观锁
$user = User::where('id', $userId)->lockForUpdate()->first();
// 或乐观锁(用 version 字段)
$affected = User::where('id', $userId)
    ->where('version', $user->version)
    ->where('points', '>=', $deductPoints)
    ->update([
        'points' => DB::raw("points - {$deductPoints}"),
        'version' => $user->version + 1
    ]);
if (!$affected) {
    throw new \Exception('积分更新失败,请重试');
}

前端交互示例(Vue3)

<template>
  <div>
    <p>订单金额:¥{{ (orderAmount / 100).toFixed(2) }}</p>
    <p>可用积分:{{ userPoints }}</p>
    <label>使用积分:</label>
    <input v-model.number="usePoints" @input="calcDeduct" />
    <span>可抵扣:¥{{ (deductAmount / 100).toFixed(2) }}</span>
    <p>最终支付:¥{{ (finalAmount / 100).toFixed(2) }}</p>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'
import axios from 'axios'
const props = defineProps({
  orderAmount: Number,  // 分
  userPoints: Number
})
const usePoints = ref(0)
const deductAmount = ref(0)
const finalAmount = ref(props.orderAmount)
const calcDeduct = async () => {
  const { data } = await axios.post('/api/order/calc-deduct', {
    order_amount: props.orderAmount,
    use_points: usePoints.value
  })
  if (data.success) {
    deductAmount.value = data.data.deduct_amount
    finalAmount.value = data.data.final_amount
  } else {
    alert(data.msg)
  }
}
</script>

最佳实践建议

  1. 统一金额单位:数据库和计算全部使用「分」为单位,避免精度问题
  2. 积分与货币分离:积分系统独立计算,不与实际货币直接挂钩,允许调整汇率
  3. 开启事务(innodb):任何涉及积分+订单的操作必须在事务中完成
  4. 配置化:汇率、上限等参数放入配置文件,方便运营调整
  5. 记录审计日志:points_log 表必须记录每笔积分的来源和去向
  6. 用户体验:前端实时展示可抵扣金额,且防止用户输入超过可用积分

这种设计可以灵活支持积分抵扣、退款退还、有效期管理等功能扩展。

抱歉,评论功能暂时关闭!