|
|
@@ -8,6 +8,7 @@ use App\Libs\Utils;
|
|
|
use App\Models\MpGeneratePicTask;
|
|
|
use App\Models\MpGenerateVideoTask;
|
|
|
use App\Models\MpUserPointsDetail;
|
|
|
+use App\Transformer\Points\PointsTransformer;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
/**
|
|
|
@@ -503,65 +504,59 @@ class PointsService
|
|
|
*/
|
|
|
public function getUserPointsRecords(array $params = []): array
|
|
|
{
|
|
|
- $uid = (int)getProp($params, 'uid', 0);
|
|
|
- if (!$uid) {
|
|
|
- $uid = (int)Site::getUid();
|
|
|
- }
|
|
|
-
|
|
|
- $type = getProp($params, 'type', '');
|
|
|
- $startDate = getProp($params, 'start_date', '');
|
|
|
- $endDate = getProp($params, 'end_date', '');
|
|
|
$pageSize = (int)getProp($params, 'page_size', 15);
|
|
|
if ($pageSize < 1 || $pageSize > 100) {
|
|
|
$pageSize = 15;
|
|
|
}
|
|
|
|
|
|
- $query = MpUserPointsDetail::where('uid', $uid);
|
|
|
- if ($type) {
|
|
|
- $query->where('type', $type);
|
|
|
- }
|
|
|
- if ($startDate) {
|
|
|
- $query->where('created_at', '>=', $startDate . ' 00:00:00');
|
|
|
- }
|
|
|
- if ($endDate) {
|
|
|
- $query->where('created_at', '<=', $endDate . ' 23:59:59');
|
|
|
- }
|
|
|
+ // 基础查询(权限 + 筛选条件),records 与 summary 共用,保证汇总随筛选条件变化
|
|
|
+ [$base, $uid] = $this->buildPointsRecordsQuery($params);
|
|
|
|
|
|
- $records = $query->orderBy('created_at', 'desc')
|
|
|
- ->orderBy('id', 'desc')
|
|
|
+ $records = (clone $base)->orderBy('d.created_at', 'desc')
|
|
|
+ ->orderBy('d.id', 'desc')
|
|
|
->paginate($pageSize);
|
|
|
|
|
|
- // 汇总统计:当前积分余额、累计消耗/退回积分、累计消耗token
|
|
|
- // 排除带 test_mode 标记的测试记录,保证统计口径与用户表实际余额一致
|
|
|
+ // 汇总统计:随筛选条件变化;除 total_points_consumed 外,均排除带 test_mode 标记的测试记录
|
|
|
$excludeTestRecords = function ($query) {
|
|
|
- $query->whereNull('charge_info')
|
|
|
- ->orWhereNull('charge_info->test_mode')
|
|
|
- ->orWhere('charge_info->test_mode', '!=', 'true');
|
|
|
+ $query->whereNull('d.charge_info')
|
|
|
+ ->orWhereNull('d.charge_info->test_mode')
|
|
|
+ ->orWhere('d.charge_info->test_mode', '!=', 'true');
|
|
|
};
|
|
|
|
|
|
- $user = DB::table('mp_manage_users')->where('id', $uid)->first();
|
|
|
+ // points_balance 始终为当前登录用户自己的余额,与查询目标无关
|
|
|
+ $loginUid = (int)Site::getUid();
|
|
|
+ $user = $loginUid ? DB::table('mp_manage_users')->where('id', $loginUid)->first() : null;
|
|
|
$summary = [
|
|
|
'points_balance' => (int)getProp($user, 'points', 0),
|
|
|
- 'total_points_consumed' => (int)MpUserPointsDetail::where('uid', $uid)
|
|
|
- ->where('points_consumed', '>', 0)
|
|
|
+ // 消耗积分(不排除测试数据,全量口径)
|
|
|
+ 'total_points_consumed' => (int)(clone $base)
|
|
|
+ ->where('d.points_consumed', '>', 0)
|
|
|
+ ->sum('d.points_consumed'),
|
|
|
+ // 消耗积分(排除测试数据)
|
|
|
+ 'total_points_consumed_filter_test' => (int)(clone $base)
|
|
|
+ ->where('d.points_consumed', '>', 0)
|
|
|
->where($excludeTestRecords)
|
|
|
- ->sum('points_consumed'),
|
|
|
- 'total_points_refunded' => (int)MpUserPointsDetail::where('uid', $uid)
|
|
|
- ->where('points_consumed', '<', 0)
|
|
|
- ->whereNotIn('type', [MpUserPointsDetail::TYPE_SYSTEM, MpUserPointsDetail::TYPE_COMPANY])
|
|
|
+ ->sum('d.points_consumed'),
|
|
|
+ 'total_points_refunded' => (int)(clone $base)
|
|
|
+ ->where('d.points_consumed', '<', 0)
|
|
|
+ ->whereNotIn('d.type', [MpUserPointsDetail::TYPE_SYSTEM, MpUserPointsDetail::TYPE_COMPANY])
|
|
|
+ ->sum('d.points_consumed'),
|
|
|
+ 'total_points_granted' => (int)abs((clone $base)
|
|
|
+ ->whereIn('d.type', [MpUserPointsDetail::TYPE_SYSTEM, MpUserPointsDetail::TYPE_COMPANY])
|
|
|
+ ->where('d.points_consumed', '<', 0)
|
|
|
+ ->sum('d.points_consumed')),
|
|
|
+ // 消耗 token(不排除测试数据,全量口径)
|
|
|
+ 'total_tokens_consumed' => (int)(clone $base)
|
|
|
+ ->sum('d.tokens_consumed'),
|
|
|
+ // 消耗 token(排除测试数据)
|
|
|
+ 'total_tokens_consumed_filter_test' => (int)(clone $base)
|
|
|
->where($excludeTestRecords)
|
|
|
- ->sum('points_consumed'),
|
|
|
- 'total_points_granted' => (int)abs(MpUserPointsDetail::where('uid', $uid)
|
|
|
- ->whereIn('type', [MpUserPointsDetail::TYPE_SYSTEM, MpUserPointsDetail::TYPE_COMPANY])
|
|
|
- ->where('points_consumed', '<', 0)
|
|
|
+ ->sum('d.tokens_consumed'),
|
|
|
+ 'total_count' => (int)(clone $base)
|
|
|
+ ->count('d.id'),
|
|
|
+ 'total_count_filter_test' => (int)(clone $base)
|
|
|
->where($excludeTestRecords)
|
|
|
- ->sum('points_consumed')),
|
|
|
- 'total_tokens_consumed' => (int)MpUserPointsDetail::where('uid', $uid)
|
|
|
- ->where($excludeTestRecords)
|
|
|
- ->sum('tokens_consumed'),
|
|
|
- 'total_count' => (int)MpUserPointsDetail::where('uid', $uid)
|
|
|
- ->where($excludeTestRecords)
|
|
|
- ->count(),
|
|
|
+ ->count('d.id'),
|
|
|
];
|
|
|
|
|
|
return [
|
|
|
@@ -571,6 +566,153 @@ class PointsService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 导出积分明细 CSV(与列表筛选条件一致:uid/type/start_date/end_date)
|
|
|
+ *
|
|
|
+ * 列:类型、模型、积分变动、变动前、变动后、Token、备注、创建时间
|
|
|
+ * 末尾追加总计行:累计消耗积分 / 累计返还积分(基于筛选后的数据)
|
|
|
+ *
|
|
|
+ * @param array $params
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ public function exportPointsRecords(array $params): void
|
|
|
+ {
|
|
|
+ // 权限与筛选条件与列表一致
|
|
|
+ [$base] = $this->buildPointsRecordsQuery($params);
|
|
|
+ $startDate = (string)getProp($params, 'start_date', '');
|
|
|
+ $endDate = (string)getProp($params, 'end_date', '');
|
|
|
+
|
|
|
+ $rows = (clone $base)->orderBy('d.created_at', 'desc')
|
|
|
+ ->orderBy('d.id', 'desc')
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ $headers = ['账号', '类型', '模型', '积分变动', '变动前', '变动后', 'Token', '备注', '创建时间'];
|
|
|
+ $csvRows = [];
|
|
|
+ $totalConsumed = 0;
|
|
|
+ $totalRefunded = 0;
|
|
|
+
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ $chargeInfo = getProp($row, 'charge_info');
|
|
|
+ if (is_string($chargeInfo)) {
|
|
|
+ $chargeInfo = json_decode($chargeInfo, true);
|
|
|
+ }
|
|
|
+ $chargeInfo = is_array($chargeInfo) ? $chargeInfo : [];
|
|
|
+
|
|
|
+ $typeCode = (string)getProp($row, 'type', '');
|
|
|
+ $pointsConsumed = (float)getProp($row, 'points_consumed', 0);
|
|
|
+ if ($pointsConsumed > 0) {
|
|
|
+ $pointsChange = '-' . $pointsConsumed;
|
|
|
+ $totalConsumed += $pointsConsumed;
|
|
|
+ } elseif ($pointsConsumed < 0) {
|
|
|
+ $pointsChange = '+' . abs($pointsConsumed);
|
|
|
+ // 返还 = 非发放(system/company)的负值记录,即退款类
|
|
|
+ if (!in_array($typeCode, [MpUserPointsDetail::TYPE_SYSTEM, MpUserPointsDetail::TYPE_COMPANY], true)) {
|
|
|
+ $totalRefunded += abs($pointsConsumed);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $pointsChange = '0';
|
|
|
+ }
|
|
|
+
|
|
|
+ $csvRows[] = [
|
|
|
+ (string)getProp($row, 'account', ''),
|
|
|
+ PointsTransformer::TYPE_LABELS[$typeCode] ?? $typeCode,
|
|
|
+ (string)getProp($chargeInfo, 'model', ''),
|
|
|
+ $pointsChange,
|
|
|
+ (string)(float)getProp($row, 'points_before', 0),
|
|
|
+ (string)(float)getProp($row, 'points_after', 0),
|
|
|
+ (string)(int)getProp($row, 'tokens_consumed', 0),
|
|
|
+ (string)getProp($row, 'remark', ''),
|
|
|
+ (string)transDate(getProp($row, 'created_at')),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 总计(最后一行单行展示)
|
|
|
+ $csvRows[] = ['总计', '', '', '', '', '', '', '累计消耗积分:' . (int)$totalConsumed . ';累计返还积分:' . (int)$totalRefunded, ''];
|
|
|
+
|
|
|
+ $start = $startDate ?: date('Y-m-d');
|
|
|
+ $end = $endDate ?: date('Y-m-d');
|
|
|
+ exportCsv('points_records_' . str_replace('-', '', $start) . '_' . str_replace('-', '', $end), $headers, $csvRows);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 当前角色可查看的积分明细范围:superadmin 全部;admin 本组织;user 仅自己;其他抛错。
|
|
|
+ *
|
|
|
+ * @return array ['cpid' => int 组织限定(0 不限定), 'uid' => int 用户限定(0 不限定)]
|
|
|
+ */
|
|
|
+ private function assertCanViewPoints(): array
|
|
|
+ {
|
|
|
+ $role = (string)Site::getRole();
|
|
|
+ if ($role === 'superadmin') {
|
|
|
+ return ['cpid' => 0, 'uid' => 0];
|
|
|
+ }
|
|
|
+ if ($role === 'admin') {
|
|
|
+ return ['cpid' => (int)Site::getCpid(), 'uid' => 0];
|
|
|
+ }
|
|
|
+ if ($role === 'user') {
|
|
|
+ return ['cpid' => (int)Site::getCpid(), 'uid' => (int)Site::getUid()];
|
|
|
+ }
|
|
|
+ Utils::throwError('1005:无权查看积分明细');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建积分明细查询(权限范围 + uid/type/日期/nickname 筛选)。
|
|
|
+ *
|
|
|
+ * @param array $params
|
|
|
+ * @return array [查询构建器, 生效的 uid(0 表示多用户视角)]
|
|
|
+ */
|
|
|
+ private function buildPointsRecordsQuery(array $params): array
|
|
|
+ {
|
|
|
+ $scope = $this->assertCanViewPoints();
|
|
|
+
|
|
|
+ $uid = (int)getProp($params, 'uid', 0);
|
|
|
+ $nickname = trim((string)getProp($params, 'nickname', ''));
|
|
|
+
|
|
|
+ // 普通用户仅能查看自己,忽略传入的 uid/nickname 筛选
|
|
|
+ if ($scope['uid'] > 0) {
|
|
|
+ $uid = $scope['uid'];
|
|
|
+ } elseif ($scope['cpid'] > 0 && !$uid && $nickname === '') {
|
|
|
+ // 管理员默认查看自己的记录;传了 uid 或 nickname 时按对应条件查组内
|
|
|
+ $uid = (int)Site::getUid();
|
|
|
+ }
|
|
|
+
|
|
|
+ $base = MpUserPointsDetail::from('mp_user_points_details as d')
|
|
|
+ ->leftJoin('mp_manage_users as u', 'u.id', '=', 'd.uid')
|
|
|
+ ->select('d.*', 'u.account');
|
|
|
+
|
|
|
+ if ($uid) {
|
|
|
+ $base->where('d.uid', $uid);
|
|
|
+ }
|
|
|
+ // 组织范围以用户表当前所属组织为准(与 token 统计口径一致)
|
|
|
+ if ($scope['cpid'] > 0) {
|
|
|
+ $base->where('u.cpid', $scope['cpid']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 昵称/账号模糊搜索(与 token 统计一致);普通用户仅查自己,忽略 nickname 避免异常传参查不到数据
|
|
|
+ if ($nickname !== '' && $scope['uid'] <= 0) {
|
|
|
+ $base->where(function ($q) use ($nickname) {
|
|
|
+ $q->where('u.nickname', 'like', '%' . $nickname . '%')
|
|
|
+ ->orWhere('u.account', 'like', '%' . $nickname . '%');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ $type = getProp($params, 'type', '');
|
|
|
+ if ($type) {
|
|
|
+ $base->where('d.type', $type);
|
|
|
+ }
|
|
|
+
|
|
|
+ $startDate = getProp($params, 'start_date', '');
|
|
|
+ if ($startDate) {
|
|
|
+ $base->where('d.created_at', '>=', $startDate . ' 00:00:00');
|
|
|
+ }
|
|
|
+
|
|
|
+ $endDate = getProp($params, 'end_date', '');
|
|
|
+ if ($endDate) {
|
|
|
+ $base->where('d.created_at', '<=', $endDate . ' 23:59:59');
|
|
|
+ }
|
|
|
+
|
|
|
+ return [$base, $uid];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 发放/回收积分(公共方法)
|
|
|
*
|
|
|
* 权限规则:
|