2 Commits c16a3d4a3c ... 472188adc0

Author SHA1 Message Date
  PC-7\Administrator 472188adc0 更新版本1 7 months ago
  “陈博论” 016d816107 增加积分消耗数 8 months ago

+ 3 - 1
app/admin/controller/package/Package.php

@@ -334,6 +334,7 @@ class Package extends Backend
         $cardType = $this->request->post('card_type', 1);
         $unitValue = $this->request->post('unit_value', 0);
         $pointsValue = $this->request->post('points_value', 0);
+        $cost_value = $this->request->post('cost_value', 0);
         $description = $this->request->post('description', '');
         $image = $this->request->post('image', '');
         $sort = $this->request->post('sort', 0);
@@ -351,7 +352,7 @@ class Package extends Backend
             $this->error(__('次卡类型必须填写有效次数'));
         }
 
-        if ($cardType == 2 && $pointsValue <= 0) {
+        if ($cardType == 2 && $pointsValue <= 0&&$cost_value<=0) {
             $this->error(__('积分类型必须填写有效积分数量'));
         }
 
@@ -387,6 +388,7 @@ class Package extends Backend
             'card_type' => $cardType,
             'unit_value' => $unitValue,
             'points_value' => $pointsValue,
+            'cost_value' => $cost_value,
             'description' => $description,
             'sort' => $sort,
             'update_time' => date('Y-m-d H:i:s')

+ 134 - 1
app/api/controller/Index.php

@@ -11,7 +11,7 @@ use app\common\library\token\TokenExpirationException;
 
 class Index extends Frontend
 {
-    protected array $noNeedLogin = ['index'];
+    protected array $noNeedLogin = ['index','checkExpiredProducts'];
 
     public function initialize(): void
     {
@@ -81,4 +81,137 @@ class Index extends Frontend
             'menus'            => $menus,
         ]);
     }
+
+
+    /*
+    * 批量更新过期记录
+    */
+    /**
+     * 检查并更新过期套餐
+     * @return int 更新的记录数
+     */
+    public function checkExpiredProducts()
+    {
+        $now = date('Y-m-d H:i:s');
+
+        Db::startTrans();
+        try {
+            // 1. 查询所有已过期且状态仍有效的 usage 记录(加锁)
+            $expiredList = Db::table('ba_user_product_usage')
+                ->where('status', 1)
+                ->where('end_time', '<=', $now)
+                ->whereNotNull('end_time')
+                ->lock(true)
+                ->select()
+                ->toArray();
+
+            if (empty($expiredList)) {
+                Db::commit();
+                return 0;
+            }
+
+            $ids = array_column($expiredList, 'id');
+
+            // 提取企业套餐的 allocation_id
+            $allocationIds = [];
+            foreach ($expiredList as $item) {
+                if (!empty($item['allocation_id'])) {
+                    $allocationIds[] = $item['allocation_id'];
+                }
+            }
+            $allocationIds = array_unique($allocationIds);
+
+            // 2. 更新 usage 表状态
+            Db::table('ba_user_product_usage')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => 2,
+                    'update_time' => $now
+                ]);
+
+            // 3. 更新企业套餐分配记录状态(如果业务需要)
+            if (!empty($allocationIds)) {
+                Db::table('ba_company_package_allocation')
+                    ->whereIn('id', $allocationIds)
+                    ->where('allocation_status', 1) // 假设 1 表示已分配/有效
+                    ->update([
+                        'allocation_status' => 2,   // 2 表示已过期或取消
+                        'update_time' => $now
+                    ]);
+
+                // 可选:更新企业套餐席位表的 assigned_seats / available_seats
+                // 这需要根据分配记录找到对应的企业套餐席位,然后减少 assigned_seats 并增加 available_seats
+                // 此处省略,可根据实际业务补充
+            }
+
+            // 4. 处理个人套餐的 ba_user_product 表
+            $personalCombos = [];
+            foreach ($expiredList as $item) {
+                if ($item['package_type'] == 1) { // 个人套餐
+                    $key = $item['user_id'] . '_' . $item['product_id'] . '_' . $item['package_price_id'];
+                    $personalCombos[$key] = [
+                        'user_id'          => $item['user_id'],
+                        'product_id'       => $item['product_id'],
+                        'package_price_id' => $item['package_price_id']
+                    ];
+                }
+            }
+
+            foreach ($personalCombos as $combo) {
+                // 检查该组合是否还有任何有效的 usage 记录
+                $hasValid = Db::table('ba_user_product_usage')
+                    ->where('user_id', $combo['user_id'])
+                    ->where('product_id', $combo['product_id'])
+                    ->where('package_price_id', $combo['package_price_id'])
+                    ->where('package_type', 1)
+                    ->where('status', 1)
+                    ->where(function ($query) use ($now) {
+                        $query->where('end_time', '>', $now)
+                            ->whereOr('end_time', 'null'); // 永久有效
+                    })
+                    ->count();
+
+                if ($hasValid == 0) {
+                    // 无有效记录,将汇总表状态置为过期
+                    Db::table('ba_user_product')
+                        ->where('user_id', $combo['user_id'])
+                        ->where('product_id', $combo['product_id'])
+                        ->where('package_price_id', $combo['package_price_id'])
+                        ->update([
+                            'status' => 2,               // 已过期
+                            'update_time' => $now
+                        ]);
+                } else {
+                    // 还有有效记录,更新 expire_time 为最新的有效记录
+                    $latestEndTime = Db::table('ba_user_product_usage')
+                        ->where('user_id', $combo['user_id'])
+                        ->where('product_id', $combo['product_id'])
+                        ->where('package_price_id', $combo['package_price_id'])
+                        ->where('package_type', 1)
+                        ->where('status', 1)
+                        ->order('end_time', 'desc')
+                        ->value('end_time');
+
+                    if ($latestEndTime) {
+                        Db::table('ba_user_product')
+                            ->where('user_id', $combo['user_id'])
+                            ->where('product_id', $combo['product_id'])
+                            ->where('package_price_id', $combo['package_price_id'])
+                            ->update([
+                                'expire_time' => $latestEndTime,
+                                'update_time' => $now
+                            ]);
+                    }
+                }
+            }
+
+            Db::commit();
+
+            return count($expiredList);
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            throw $e;
+        }
+    }
 }

+ 2 - 1
app/api/controller/invitation/Order.php

@@ -59,7 +59,7 @@ class Order extends Frontend
             }
 
             if (!empty($star_time) && !empty($end_time)) {
-                $where[] = ['order_time', 'between', [$star_time, $end_time]];
+                $where[] = ['order_time', 'between', [$star_time.' 00:00:00', $end_time.' 23:59:59']];
                 $query->where($where); // 塞选时间
             } else {
                 $yesterday = date('Y-m-d 00:00:00', strtotime('-1 day'));
@@ -70,6 +70,7 @@ class Order extends Frontend
 
             // 获取订单总数
             $total = $query->count();
+
             // 获取订单列表(只取必要字段)
             $orders = $query->field('order_no, pay_amount, pay_method, pay_status, order_time, pay_time')
                 ->order('order_time', 'desc')

+ 64 - 1
app/api/controller/invitation/Product.php

@@ -7,7 +7,7 @@ use think\facade\Db;
 
 class Product extends Frontend
 {
-    protected array $noNeedPermission = ['purchasedList', 'detail', 'myProducts'];
+    protected array $noNeedPermission = ['purchasedList', 'detail', 'myProducts','test_project'];
     protected $userProductService;
 
     public function initialize(): void
@@ -58,6 +58,37 @@ class Product extends Frontend
         $this->myProducts();
     }
 
+
+    /**
+     * @return void
+     * 判断用户是否有使用这个产品的权限
+     */
+    public function test_project(): void
+    {
+        $userId = $this->auth->id;
+        if (!$userId) {
+            $this->error('用户未登录');
+        }
+        $productId = $this->request->get('project_id', 0);
+        if ($productId <= 0) {
+            $this->error('产品ID错误');
+        }
+        $products = $this->userProductService->getUserProducts($userId);
+        foreach ($products as $v){
+
+            if ($v['product_id'] == $productId) {
+                // 找到匹配项,直接返回数据并终止循环
+                $this->success('用户已购买产品',['status'=>'true']);
+                // 或者如果只是想终止循环并返回数据,也可以用 break + return
+                // break;
+            }
+
+        }
+          // 如果循环结束都没找到,返回未找到提示
+         $this->error('未找到对应产品,请购买对应的产品',['status'=>'false']);
+    }
+
+
     /**
      * 获取产品详情(包含购买记录)
      */
@@ -144,6 +175,37 @@ class Product extends Frontend
         return '';
     }
 
+
+    /**
+     * 用户使用产品接口
+     * @ApiMethod (POST)
+     * @ApiParams (name="product_id", type="integer", required=true, description="产品ID")
+     */
+    public function useProduct()
+    {
+        $userId = $this->auth->id;
+        if (!$userId) {
+            $this->error('用户未登录');
+        }
+
+        $productId = $this->request->post('product_id', 0);
+        if ($productId <= 0) {
+            $this->error('产品ID不能为空');
+        }
+//        dump($userId);
+//        dump($productId);die;
+        try {
+            $result = $this->userProductService->useProductByProductId($userId, $productId);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        if ($result) {
+            $this->success('使用成功');
+        } else {
+            $this->error('使用失败');
+        }
+    }
+
     /**
      * 获取卡类型文本
      */
@@ -160,4 +222,5 @@ class Product extends Frontend
         ];
         return $map[$type] ?? '其他';
     }
+
 }

+ 0 - 2
app/api/controller/product/Purchase.php

@@ -75,7 +75,6 @@ class Purchase extends Frontend
         }else{
             $companyId =0;
         }
-
             // 查找关联的产品
             $product = Db::table('ba_product')
                 ->where('package_id', $package['package_id'])
@@ -137,7 +136,6 @@ class Purchase extends Frontend
                 ];
 
                 Db::table('ba_order_item')->insert($itemData);
-
                 // 3. 如果存在分销,创建分销记录
                 if ($hasCommission && $distributorId > 0) {
                     $commissionService->createDistributionRecord([

+ 2 - 0
app/common/service/CompanyPackageService.php

@@ -1476,4 +1476,6 @@ class CompanyPackageService
         }
     }
 
+
+
 }

+ 161 - 2
app/common/service/UserProductService.php

@@ -205,7 +205,7 @@ class UserProductService
                 ->leftJoin('ba_package_price pp', 'pp.id = upu.package_price_id')
                 ->where('upu.user_id', $userId)
                 ->where('upu.package_type', 1) // 个人套餐
-                ->where('upu.status', 1) // 有效
+//                ->where('upu.status', 1) // 有效
                 ->field('upu.*, 
                     p.name as product_name, p.type as product_type, p.link,
                     pp.spec_name, pp.type as package_type, pp.card_type,pp.image')
@@ -219,7 +219,7 @@ class UserProductService
                 ->leftJoin('ba_company_package_allocation cpa', 'cpa.id = upu.allocation_id')
                 ->where('upu.user_id', $userId)
                 ->where('upu.package_type', 2) // 企业套餐
-                ->where('upu.status', 1) // 有效
+//                ->where('upu.status', 1) // 有效
                 ->where('upu.is_allocated', 1) // 已分配
                 ->field('upu.*,
                     p.name as product_name, p.type as product_type, p.link,
@@ -265,6 +265,165 @@ class UserProductService
         }
     }
 
+
+    /**
+     * 用户使用产品(根据产品ID自动匹配可用套餐,优先级:日期卡 > 次卡 > 积分卡)
+     * @param int $userId
+     * @param int $productId
+     * @return bool
+     * @throws \Exception
+     */
+    public function useProductByProductId($userId, $productId)
+    {
+        Db::startTrans();
+        try {
+            // 1. 获取该产品关联的所有套餐规格ID
+            $packagePriceIds = Db::table('ba_product')
+                ->alias('p')
+                ->leftJoin('ba_package_price pp', 'pp.package_id = p.package_id')
+                ->where('p.id', $productId)
+                ->where('p.status', 1)
+                ->column('pp.id');
+
+            if (empty($packagePriceIds)) {
+                throw new \Exception('产品未配置套餐');
+            }
+
+            $now = date('Y-m-d H:i:s');
+
+            // 2. 查询用户所有可能有效的使用记录(加锁防止并发)
+            $usageList = Db::table('ba_user_product_usage')
+                ->where('user_id', $userId)
+                ->whereIn('package_price_id', $packagePriceIds)
+                ->where('status', 1) // 有效
+                ->lock(true)
+                ->select()
+                ->toArray();
+
+            if (empty($usageList)) {
+                throw new \Exception('没有可用的套餐');
+            }
+
+            // 3. 批量获取所有套餐规格的 cost_value 映射(用于积分卡)
+            $priceIds = array_column($usageList, 'package_price_id');
+            $costMap = Db::table('ba_package_price')
+                ->whereIn('id', $priceIds)
+                ->column('cost_value', 'id');
+
+            // 4. 按卡类型分类并过滤出真正可用的记录
+            $dateCardUsages = [];   // 日期卡 (card_type >= 3)
+            $timesCardUsages = [];  // 次卡 (card_type = 1)
+            $pointsCardUsages = []; // 积分卡 (card_type = 2)
+
+            foreach ($usageList as $usage) {
+                $cardType = $usage['card_type'];
+                if ($cardType >= 3) { // 日期卡
+                    if (empty($usage['end_time']) || $usage['end_time'] > $now) {
+                        $dateCardUsages[] = $usage;
+                    }
+                } elseif ($cardType == 1) { // 次卡
+                    if ($usage['remaining_times'] > 0) {
+                        $timesCardUsages[] = $usage;
+                    }
+                } elseif ($cardType == 2) { // 积分卡
+                    $cost = $costMap[$usage['package_price_id']] ?? 0;
+                    if ($cost > 0 && $usage['remaining_points'] >= $cost) {
+                        $pointsCardUsages[] = $usage;
+                    }
+                }
+            }
+
+            // 5. 按优先级选择一条记录
+            $selectedUsage = null;
+            $consumeValue = 0; // 积分卡本次消耗值
+
+            if (!empty($dateCardUsages)) {
+                // 日期卡按到期时间升序,优先使用即将到期的
+                usort($dateCardUsages, function ($a, $b) {
+                    $aEnd = $a['end_time'] ?? '9999-12-31 23:59:59';
+                    $bEnd = $b['end_time'] ?? '9999-12-31 23:59:59';
+                    return strcmp($aEnd, $bEnd);
+                });
+                $selectedUsage = $dateCardUsages[0];
+            } elseif (!empty($timesCardUsages)) {
+                // 次卡按剩余次数升序,优先使用剩余少的
+                usort($timesCardUsages, function ($a, $b) {
+                    return $a['remaining_times'] <=> $b['remaining_times'];
+                });
+                $selectedUsage = $timesCardUsages[0];
+            } elseif (!empty($pointsCardUsages)) {
+                // 积分卡按剩余积分升序,优先使用剩余少的
+                usort($pointsCardUsages, function ($a, $b) {
+                    return $a['remaining_points'] <=> $b['remaining_points'];
+                });
+                $selectedUsage = $pointsCardUsages[0];
+                $consumeValue = $costMap[$selectedUsage['package_price_id']];
+            } else {
+                throw new \Exception('没有可用的套餐');
+            }
+
+            // 6. 执行扣减操作
+            $updateData = [
+                'last_use_time' => $now,
+                'update_time'   => $now
+            ];
+
+            $cardType = $selectedUsage['card_type'];
+            switch ($cardType) {
+                case 1: // 次卡
+                    $updateData['remaining_times'] = Db::raw('remaining_times - 1');
+                    if ($selectedUsage['remaining_times'] - 1 <= 0) {
+                        $updateData['status'] = 3; // 已用完
+                    }
+                    break;
+                case 2: // 积分卡
+                    $updateData['remaining_points'] = Db::raw('remaining_points - ' . $consumeValue);
+                    if ($selectedUsage['remaining_points'] - $consumeValue <= 0) {
+                        $updateData['status'] = 3; // 已用完
+                    }
+                    break;
+                // 日期卡无需扣减,仅记录使用时间
+            }
+
+            Db::table('ba_user_product_usage')
+                ->where('id', $selectedUsage['id'])
+                ->update($updateData);
+
+            // 7. 记录使用明细日志(可选)
+            $this->logUsage($userId, $selectedUsage['id'], $productId, $consumeValue, $selectedUsage);
+
+            Db::commit();
+            return true;
+
+        } catch (\Throwable $e) {
+            Db::rollback();
+            Log::error('用户使用产品失败:' . $e->getMessage(), [
+                'user_id'    => $userId,
+                'product_id' => $productId
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 记录使用明细日志
+     */
+    private function logUsage($userId, $usageId, $productId, $consumeValue, $usage)
+    {
+        $logData = [
+            'user_id'       => $userId,
+            'usage_id'      => $usageId,
+            'product_id'    => $productId,
+            'consume_type'  => $usage['card_type'] == 1 ? 1 : 2, // 1=次数,2=积分
+            'consume_value' => $consumeValue,
+            'before_value'  => $usage['card_type'] == 1 ? $usage['remaining_times'] : $usage['remaining_points'],
+            'after_value'   => $usage['card_type'] == 1 ? $usage['remaining_times'] - 1 : $usage['remaining_points'] - $consumeValue,
+            'create_time'   => date('Y-m-d H:i:s')
+        ];
+        Db::table('ba_user_usage_log')->insert($logData);
+    }
+
+
     /**
      * 格式化产品项
      */

+ 4 - 2
web/src/views/backend/package/package/detail.vue

@@ -143,7 +143,7 @@
                         <el-table-column
                             label="次数/积分"
                             align="center"
-                            min-width="120"
+                            min-width="175"
                         >
                             <template #default="{ row }">
                                 <span v-if="String(row.card_type) === '1'">
@@ -152,7 +152,9 @@
                                 </span>
                                 <span v-else-if="String(row.card_type) === '2'">
                                     <span class="points-value">{{ row.points_value || '-' }}</span>
-                                    <span class="unit-label">分</span>
+                                    <span class="unit-label">积分</span>/
+                                    <span class="points-value">{{ row.cost_value || '-' }}</span>
+                                    <span class="unit-label">消耗积分</span>
                                 </span>
                                 <span v-else class="no-value">-</span>
                             </template>

+ 3 - 3
web/src/views/backend/package/package/popupForm.vue

@@ -1,7 +1,7 @@
 <template>
 	<!-- 对话框表单 -->
 	<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
-		@close="handleClose" width="880px">
+		@close="handleClose" width="1100px">
 		<template #header>
 			<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
 				套餐配置
@@ -56,13 +56,13 @@
 											{{ getCardTypeLabel(scope.row.card_type) }}
 										</template>
 									</el-table-column>
-									<el-table-column label="次数/积分" align="center" min-width="100">
+									<el-table-column label="次数/积分" align="center" min-width="160">
 										<template #default="scope">
 											<span v-if="scope.row.card_type == '1' && scope.row.unit_value">
 												{{ scope.row.unit_value }}次
 											</span>
 											<span v-else-if="scope.row.card_type == '2' && scope.row.points_value">
-												{{ scope.row.points_value }}分
+												{{ scope.row.points_value }}积分/{{ scope.row.cost_value }}消耗积分
 											</span>
 											<span v-else>-</span>
 										</template>

+ 68 - 20
web/src/views/backend/package/package/specDialog.vue

@@ -120,23 +120,46 @@
 						<div class="form-tip">当选择次卡时必填</div>
 					</el-form-item>
 					
-					<el-form-item
-						v-if="formData.card_type === '2'"
-						label="积分数"
-						prop="points_value"
-					>
-						<div class="input-with-unit">
-							<el-input-number
-								v-model.number="formData.points_value"
-								:min="1"
-								placeholder="请输入积分数"
-								style="width: 100%"
-								controls-position="right"
-							/>
-							<span class="form-unit">分</span>
-						</div>
-						<div class="form-tip">当选择按积分时必填</div>
-					</el-form-item>
+					<el-row v-if="formData.card_type === '2'" :gutter="20">
+						<el-col :span="12">
+							<el-form-item
+								label="积分数"
+								prop="points_value"
+							>
+								<div class="input-with-unit">
+									<el-input-number
+										v-model.number="formData.points_value"
+										:min="1"
+										placeholder="请输入积分数"
+										style="width: 100%"
+										controls-position="right"
+									/>
+									<span class="form-unit">积分</span>
+								</div>
+								<div class="form-tip">当选择按积分时必填</div>
+							</el-form-item>
+						</el-col>
+						
+						<!-- 使用消耗积分数 -->
+						<el-col :span="12">
+							<el-form-item
+								label="消耗积分"
+								prop="cost_value"
+							>
+								<div class="input-with-unit">
+									<el-input-number
+										v-model.number="formData.cost_value"
+										:min="1"
+										placeholder="请输入单次消耗积分数"
+										style="width: 100%"
+										controls-position="right"
+									/>
+									<span class="form-unit">积分</span>
+								</div>
+								<div class="form-tip">每次使用将扣减的积分数</div>
+							</el-form-item>
+						</el-col>
+					</el-row>
 					
 					<el-form-item label="说明描述" prop="description">
 						<el-input
@@ -195,6 +218,8 @@ const formData = reactive({
 	card_type: '',
 	unit_value: undefined as number | undefined,
 	points_value: undefined as number | undefined,
+	// 单次消耗积分数(仅按积分卡类型使用)
+	cost_value: undefined as number | undefined,
 	description: '',
 })
 
@@ -240,6 +265,18 @@ const rules: FormRules = {
 			trigger: 'change',
 		},
 	],
+	cost_value: [
+		{
+			validator: (_rule, value, callback) => {
+				// 仅当为“按积分”卡类型时校验
+				if (formData.card_type === '2' && (value === undefined || value === null || value <= 0)) {
+					return callback(new Error('单次消耗积分数为必填项,且必须大于0'))
+				}
+				return callback()
+			},
+			trigger: 'change',
+		},
+	],
 }
 
 // 套餐类型选项
@@ -301,6 +338,8 @@ const editDialog = (pkgId: number, specData: any) => {
 	// 积分数:转换为数字,如果为空或无效则设为 undefined
 	formData.points_value = toNumber(specData.points_value) || 
 		(specData.card_type === '2' ? toNumber(specData.quantity) : undefined)
+	// 单次消耗积分数
+	formData.cost_value = toNumber(specData.cost_value) || toNumber(specData.costValue)
 	formData.description = specData.description || specData.remark || ''
 }
 
@@ -315,6 +354,7 @@ const resetForm = () => {
 	formData.card_type = ''
 	formData.unit_value = undefined
 	formData.points_value = undefined
+	formData.cost_value = undefined
 	formData.description = ''
 }
 
@@ -326,6 +366,7 @@ const handleCardTypeChange = () => {
 	}
 	if (formData.card_type !== '2') {
 		formData.points_value = undefined
+		formData.cost_value = undefined
 	}
 }
 
@@ -356,9 +397,14 @@ const onSubmit = async () => {
 		if (formData.card_type === '1' && formData.unit_value !== undefined && formData.unit_value != null) {
 			submitData.unit_value = Number(formData.unit_value)
 		}
-		// 当选择积分时必填 points_value,选择其他天数的卡可以不传
-		if (formData.card_type === '2' && formData.points_value !== undefined && formData.points_value != null) {
-			submitData.points_value = Number(formData.points_value)
+		// 当选择积分时必填 points_value 与 cost_value,选择其他天数的卡可以不传
+		if (formData.card_type === '2') {
+			if (formData.points_value !== undefined && formData.points_value != null) {
+				submitData.points_value = Number(formData.points_value)
+			}
+			if (formData.cost_value !== undefined && formData.cost_value != null) {
+				submitData.cost_value = Number(formData.cost_value)
+			}
 		}
 
 		// 调用接口
@@ -468,6 +514,8 @@ defineExpose({ openDialog, editDialog })
 		font-size: 12px;
 		color: #909399;
 		line-height: 1.5;
+		/* 强制占满一行,显示在输入框下方 */
+		width: 100%;
 	}
 	
 	:deep(.el-input__wrapper) {