初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\enum\common\EnabledStatus;
use app\model\member\MemberAddress;
/**
* 会员地址数据访问对象
*/
class MemberAddressDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberAddress::class;
}
/**
* 设置默认地址
*
* @throws \Exception
*/
public function setDefault(MemberAddress $address): bool
{
// 先将该会员的所有地址设置为非默认
$this->query()->where('member_id', $address->member_id)->update(['is_default' => 0]);
// 将当前地址设置为默认
$address->is_default = 1;
return $address->save();
}
/**
* 获取会员默认地址
*/
public function getDefaultAddress(int $memberId): ?MemberAddress
{
return $this->query()
->where('member_id', $memberId)
->where('is_default', 1)
->where('enabled', EnabledStatus::ENABLED->value)
->first();
}
/**
* 获取会员地址列表
*/
public function getMemberAddresses(int $memberId): array
{
return $this->query()
->where('member_id', $memberId)
->where('enabled', EnabledStatus::ENABLED->value)
->orderBy('is_default', 'desc')
->orderBy('updated_at', 'desc')
->get()
->toArray();
}
/**
* 创建或更新地址
*/
public function saveAddress(int $memberId, array $data): MemberAddress
{
$addressId = $data['id'] ?? 0;
if ($addressId) {
// 更新地址
$address = $this->find($addressId);
if (!$address || $address->member_id != $memberId) {
throw new \Exception('地址不存在或无权限');
}
} else {
// 创建新地址
$address = $this->getModel();
$address->member_id = $memberId;
$address->enabled = EnabledStatus::ENABLED->value;
}
// 填充数据
$fillable = [
'name',
'phone',
'province',
'city',
'district',
'address',
'zipcode',
];
foreach ($fillable as $field) {
if (isset($data[$field])) {
$address->{$field} = $data[$field];
}
}
// 设置默认地址
if (isset($data['is_default']) && $data['is_default'] == 1) {
// 先将其他地址设置为非默认
$this->query()
->where('member_id', $memberId)
->update(['is_default' => 0]);
$address->is_default = 1;
} elseif (!$addressId) {
// 新地址默认为非默认
$address->is_default = 0;
}
$address->save();
return $address;
}
/**
* 删除地址
*/
public function deleteAddress(MemberAddress $address): bool
{
$address->enabled = EnabledStatus::DISABLED->value;
return $address->save();
}
/**
* 验证地址数据
*/
public function validateAddress(array $data): array
{
$errors = [];
if (empty($data['name'])) {
$errors[] = '收货人姓名不能为空';
}
if (empty($data['phone'])) {
$errors[] = '联系电话不能为空';
} elseif (!preg_match('/^1[3-9]\d{9}$/', $data['phone'])) {
$errors[] = '联系电话格式不正确';
}
if (empty($data['province'])) {
$errors[] = '省份不能为空';
}
if (empty($data['city'])) {
$errors[] = '城市不能为空';
}
if (empty($data['district'])) {
$errors[] = '区县不能为空';
}
if (empty($data['address'])) {
$errors[] = '详细地址不能为空';
}
return $errors;
}
/**
* 获取会员地址数量
*/
public function getMemberAddressCount(int $memberId): int
{
return $this->count([
'member_id' => $memberId,
'enabled' => EnabledStatus::ENABLED->value
]);
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\model\member\MemberBill;
use app\enum\member\BillType;
use app\enum\member\BillStatus;
/**
* 会员账单DAO
*/
class MemberBillDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberBill::class;
}
/**
* 记录账单
*/
public function record(int $memberId, int $type, int $category, float $amount, string $description, string $orderSn = '', int $status = BillStatus::SUCCESS->value): MemberBill
{
$bill = $this->getModel();
$bill->member_id = $memberId;
$bill->type = $type;
$bill->category = $category;
$bill->amount = $amount;
$bill->description = $description;
$bill->order_sn = $orderSn;
$bill->status = $status;
$bill->save();
return $bill;
}
/**
* 获取会员账单列表
*/
public function getMemberBills(int $memberId, array $params = []): array
{
$query = $this->query()->where('member_id', $memberId);
// 类型筛选
if (isset($params['type'])) {
$query->where('type', $params['type']);
}
// 分类筛选
if (isset($params['category'])) {
$query->where('category', $params['category']);
}
// 时间范围筛选
if (isset($params['start_date']) && isset($params['end_date'])) {
$query->whereBetween('created_at', [
$params['start_date'] . ' 00:00:00',
$params['end_date'] . ' 23:59:59'
]);
}
// 状态筛选
if (isset($params['status'])) {
$query->where('status', $params['status']);
}
// 分页参数
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 10;
return $query->orderBy('created_at', 'desc')
->paginate($limit, ['*'], 'page', $page)
->toArray();
}
/**
* 获取会员账单统计
*/
public function getMemberBillStats(int $memberId, string $startDate, string $endDate): array
{
$stats = $this->query()
->where('member_id', $memberId)
->whereBetween('created_at', [
$startDate . ' 00:00:00',
$endDate . ' 23:59:59'
])
->selectRaw('type, category, SUM(amount) as total_amount, COUNT(*) as count')
->groupBy('type', 'category')
->get()
->toArray();
$result = [
'total_income' => 0,
'total_expense' => 0,
'category_stats' => [],
];
foreach ($stats as $stat) {
if ($stat['type'] == BillType::INCOME->value) {
$result['total_income'] += $stat['total_amount'];
} else {
$result['total_expense'] += $stat['total_amount'];
}
$result['category_stats'][$stat['category']] = [
'total_amount' => $stat['total_amount'],
'count' => $stat['count'],
];
}
return $result;
}
/**
* 获取最新账单
*/
public function getLatestBill(int $memberId): ?MemberBill
{
return $this->query()
->where('member_id', $memberId)
->orderBy('created_at', 'desc')
->first();
}
/**
* 获取今日账单统计
*/
public function getTodayStats(int $memberId): array
{
$today = date('Y-m-d');
return $this->getMemberBillStats($memberId, $today, $today);
}
/**
* 获取本月账单统计
*/
public function getMonthStats(int $memberId): array
{
$firstDay = date('Y-m-01');
$lastDay = date('Y-m-t');
return $this->getMemberBillStats($memberId, $firstDay, $lastDay);
}
/**
* 根据订单号更新账单状态
*/
public function updateStatusByOrderSn(string $orderSn, int $status): bool
{
return $this->query()
->where('order_sn', $orderSn)
->update(['status' => $status]);
}
/**
* 获取账单总数
*/
public function getCount(int $memberId, array $params = []): int
{
$query = $this->query()->where('member_id', $memberId);
// 类型筛选
if (isset($params['type'])) {
$query->where('type', $params['type']);
}
// 分类筛选
if (isset($params['category'])) {
$query->where('category', $params['category']);
}
// 状态筛选
if (isset($params['status'])) {
$query->where('status', $params['status']);
}
return $query->count();
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use app\scope\global\AccessPermissionScope;
use app\service\admin\member\MemberTagRelationService;
use app\service\admin\member\MemberTagService;
use core\base\BaseDao;
use app\model\member\Member;
use madong\query\QueryBuilderHelper;
use madong\query\QueryParamConverter;
use madong\query\enum\ParamFormat;
use InvalidArgumentException;
use support\Container;
/**
* 会员DAO
*/
class MemberDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return Member::class;
}
/**
* 获取活跃用户
*
* @param int $limit
*
* @return \Illuminate\Database\Eloquent\Collection
* @throws \Exception
*/
public function getActiveUsers(int $limit = 5): \Illuminate\Database\Eloquent\Collection
{
// last_time 字段存储为时间戳格式(dateFormat = 'U')
// 计算30天前的时间戳
$thirtyDaysAgo = time() - (30 * 24 * 60 * 60);
return $this->query()->withoutGlobalScope(AccessPermissionScope::class)
->where('last_login_time', '>=', $thirtyDaysAgo)
->orderBy('last_login_time', 'desc')
->limit($limit)
->get();
}
/**
* 根据标签ID获取用户列表
*
* @param array $where 查询条件,支持多种格式自动转换
* @param string $field 查询字段
* @param int $page 页码
* @param int $limit 每页数量
* @param array|null $withoutScopes 移除的作用域
*
* @return array ['total' => int, 'items' => array]
* @throws \Exception
*/
public function getUsersListByTagId(array $where, string $field, int $page, int $limit, ?array $withoutScopes = null): array
{
try {
//传入的是标准filters格式
$tagId = QueryParamConverter::getValue($where, 'tag_id');
if (!$tagId) {
throw new InvalidArgumentException("Tag ID is required.");
}
// 从 where 中移除 tag_id
$where = QueryParamConverter::removeFilter($where, 'tag_id');
$query = $this->query()->with(['tags' => function ($query) use ($withoutScopes) {
$this->applyScopeRemoval($query, $withoutScopes);
}])
->whereHas('tags', function ($query) use ($tagId) {
$query->where('id', $tagId);
});
$this->applyScopeRemoval($query, $withoutScopes);
// 应用剩余的 filters 条件
if (!empty($where['filters'])) {
QueryBuilderHelper::applyFiltersToQuery($query, $where['filters']);
}
$total = $query->count();
$items = $query->when($page > 0 && $limit > 0, function ($query) use ($page, $limit) {
return $query->skip(($page - 1) * $limit)->take($limit);
})
->select(explode(',', $field))
->get()
->toArray();
return compact('total', 'items');
} catch (\Throwable $e) {
throw new \Exception("获取用户列表失败: " . $e->getMessage(), 0, $e);
}
}
/**
* 排除标签ID-用户列表
*
* @param array $where
* @param string $field
* @param int $page
* @param int $limit
* @param array|null $withoutScopes
*
* @return array
* @throws \ReflectionException|\Exception
*/
public function getUsersExcludingTag(array $where, string $field, int $page, int $limit, ?array $withoutScopes = null): array
{
// 使用 QueryParamConverter 获取 tag_id 值(自动识别格式)
$tagId = QueryParamConverter::getValue($where, 'tag_id');
// 从 where 中移除 tag_id
$where = QueryParamConverter::removeFilter($where, 'tag_id');
// 获取排除的用户ID列表
if ($tagId) {
$memberTagRelationService = Container::make(MemberTagRelationService::class);
$excludedMemberUserIds = $memberTagRelationService->getColumn(['tag_id' => $tagId], 'member_id');
} else {
$excludedMemberUserIds = [];
}
$query = $this->getModel()->whereNotIn('id', $excludedMemberUserIds)
->with(['tags' => function ($query) use ($withoutScopes) {
$this->applyScopeRemoval($query, $withoutScopes);
}]);
$this->applyScopeRemoval($query, $withoutScopes);
// 应用剩余的 filters 条件
if (!empty($where['filters'])) {
QueryBuilderHelper::applyFiltersToQuery($query, $where['filters']);
}
$total = $query->count();
// 如果没有用户,则返回空结果
if ($total === 0) {
return ['total' => 0, 'items' => []];
}
$items = $query->when($page > 0 && $limit > 0, function ($query) use ($page, $limit) {
return $query->skip(($page - 1) * $limit)->take($limit);
})->select($field)->get()->toArray();
return compact('total', 'items');
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\enum\common\EnabledStatus;
use app\model\member\MemberLevel;
/**
* 会员等级数据访问对象
*/
class MemberLevelDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberLevel::class;
}
// /**
// * 根据积分获取等级
// */
// public function getLevelByPoints(int $points): ?MemberLevel
// {
// return $this->query()
// ->where('enabled', EnabledStatus::ENABLED->value)
// ->where('min_points', '<=', $points)
// ->where(function ($query) use ($points) {
// $query->where('max_points', '>=', $points)
// ->orWhere('max_points', 0);
// })
// ->orderBy('level', 'desc')
// ->first();
// }
//
// /**
// * 获取下一个等级
// */
// public function getNextLevel(MemberLevel $currentLevel): ?MemberLevel
// {
// return $this->query()
// ->where('enabled', EnabledStatus::ENABLED->value)
// ->where('level', '>', $currentLevel->level)
// ->orderBy('level', 'asc')
// ->first();
// }
//
// /**
// * 获取等级进度
// */
// public function getLevelProgress(MemberLevel $currentLevel, int $points): array
// {
// $nextLevel = $this->getNextLevel($currentLevel);
//
// if (!$nextLevel) {
// return [
// 'current_points' => $points,
// 'next_points' => 0,
// 'progress' => 100,
// 'is_max_level' => true,
// ];
// }
//
// $currentRange = $currentLevel->max_points - $currentLevel->min_points;
// $currentPoints = $points - $currentLevel->min_points;
// $progress = $currentRange > 0 ? round(($currentPoints / $currentRange) * 100, 2) : 0;
//
// return [
// 'current_points' => $points,
// 'next_points' => $nextLevel->min_points,
// 'progress' => min($progress, 100),
// 'is_max_level' => false,
// ];
// }
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use app\model\member\MemberPoints;
use core\base\BaseDao;
/**
* 会员积分DAO类
*/
class MemberPointsDao extends BaseDao
{
/**
* 设置模型
*/
public function setModel(): string
{
return MemberPoints::class;
}
/**
* 获取会员积分记录
*/
public function getMemberPoints(int|string $memberId, int $limit = 20): array
{
return $this->selectList(
[['member_id', '=', $memberId]],
'*',
0, $limit,
'created_at desc'
)->toArray();
}
/**
* 获取会员积分余额
*/
public function getMemberBalance(int|string $memberId): float
{
$lastRecord = $this->query()
->where('member_id', '=', $memberId)
->orderBy('created_at', 'desc')
->first();
return $lastRecord ? $lastRecord->balance : 0;
}
}
+250
View File
@@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use app\service\api\member\MemberService;
use core\base\BaseDao;
use app\model\member\MemberSign;
use support\Container;
/**
* 会员签到DAO
*/
class MemberSignDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberSign::class;
}
/**
* 创建签到记录
*/
public function created(int $memberId, string $signDate, int $points, int $continuousDays, array $deviceInfo = []): MemberSign
{
$sign = new MemberSign();
$sign->member_id = $memberId;
$sign->sign_date = $signDate;
$sign->points = $points;
$sign->continuous_days = $continuousDays;
$sign->save();
return $sign;
}
/**
* 检查今日是否已签到
*/
public function isSignedToday(int|string $memberId, string $today): bool
{
return $this->query()
->where('member_id', $memberId)
->where('sign_date', $today)
->exists();
}
/**
* 获取最近一次签到记录
*/
public function getLastSign(int|string $memberId): ?MemberSign
{
return $this->query()
->where('member_id', $memberId)
->orderBy('sign_date', 'desc')
->first();
}
/**
* 获取会员签到记录
*/
public function getMemberSignRecords(int|string $memberId, int $days = 30): array
{
$startDate = date('Y-m-d', strtotime("-$days days"));
return $this->query()
->where('member_id', $memberId)
->where('sign_date', '>=', $startDate)
->orderBy('sign_date', 'asc')
->get()
->toArray();
}
/**
* 获取本月签到天数
*/
public function getMonthSignDays(int|string $memberId): int
{
$firstDay = date('Y-m-01');
$lastDay = date('Y-m-t');
return $this->query()
->where('member_id', $memberId)
->whereBetween('sign_date', [$firstDay, $lastDay])
->count();
}
/**
* 获取连续签到排行榜
*/
public function getContinuousSignRanking(int $limit = 10): array
{
return $this->query()
->selectRaw('member_id, MAX(continuous_days) as max_continuous_days')
->groupBy('member_id')
->orderBy('max_continuous_days', 'desc')
->limit($limit)
->with('member')
->get()
->toArray();
}
/**
* 获取签到统计
*/
public function getSignStatistics(int|string $memberId): array
{
$totalSignDays = $this->query()
->where('member_id', $memberId)
->count();
$monthSignDays = $this->getMonthSignDays($memberId);
$continuousDays = $this->query()
->where('member_id', $memberId)
->orderBy('sign_date', 'desc')
->value('continuous_days') ?? 0;
// 获取用户积分
$points = Container::make(MemberService::class)
->get($memberId)
->points ?? 0;
return [
'total_sign_days' => $totalSignDays,
'month_sign_days' => $monthSignDays,
'continuous_days' => $continuousDays,
'today_signed' => $this->isSignedToday($memberId, date('Y-m-d')),
'points' => $points
];
}
/**
* 获取年度签到统计
*/
public function getYearSignStatistics(int|string $memberId, int $year = null): array
{
if ($year === null) {
$year = date('Y');
}
$startDate = $year . '-01-01';
$endDate = $year . '-12-31';
$totalSignDays = $this->query()
->where('member_id', $memberId)
->whereBetween('sign_date', [$startDate, $endDate])
->count();
// 统计每个月的签到天数
$monthlyStats = [];
for ($month = 1; $month <= 12; $month++) {
$monthStart = $year . '-' . str_pad((string)$month, 2, '0', STR_PAD_LEFT) . '-01';
$monthEnd = $year . '-' . str_pad((string)$month, 2, '0', STR_PAD_LEFT) . '-' . date('t', strtotime($monthStart));
$monthlyStats[$month] = $this->query()
->where('member_id', $memberId)
->whereBetween('sign_date', [$monthStart, $monthEnd])
->count();
}
return [
'year' => $year,
'total_sign_days' => $totalSignDays,
'monthly_stats' => $monthlyStats
];
}
/**
* 获取周签到统计
*/
public function getWeekSignStatistics(int|string $memberId): array
{
$weekStart = date('Y-m-d', strtotime('this week'));
$weekEnd = date('Y-m-d', strtotime('this week +6 days'));
$signDays = $this->query()
->where('member_id', $memberId)
->whereBetween('sign_date', [$weekStart, $weekEnd])
->count();
return [
'week_start' => $weekStart,
'week_end' => $weekEnd,
'sign_days' => $signDays,
'total_days' => 7
];
}
/**
* 获取连续签到奖励
*/
public function getContinuousSignReward(int|string $continuousDays): int
{
// 连续签到奖励规则
$rewards = [
7 => 50, // 连续7天奖励50积分
14 => 100, // 连续14天奖励100积分
30 => 200, // 连续30天奖励200积分
];
return $rewards[$continuousDays] ?? 0;
}
/**
* 获取签到日历
*/
public function getSignCalendar(int|string $memberId, int $year, int $month): array
{
$firstDay = $year . '-' . str_pad((string)$month, 2, '0', STR_PAD_LEFT) . '-01';
$lastDay = $year . '-' . str_pad((string)$month, 2, '0', STR_PAD_LEFT) . '-' . date('t', strtotime($firstDay));
$signs = $this->query()
->where('member_id', $memberId)
->whereBetween('sign_date', [$firstDay, $lastDay])
->get()
->toArray();
$calendar = [];
foreach ($signs as $sign) {
$calendar[] = $sign['sign_date'];
}
return $calendar;
}
/**
* 批量获取会员签到统计
*/
public function batchGetSignStatistics(array $memberIds): array
{
$result = [];
foreach ($memberIds as $memberId) {
$result[$memberId] = $this->getSignStatistics($memberId);
}
return $result;
}
/**
* 清理过期签到记录(保留最近一年的记录)
*/
public function cleanExpiredRecords(): int
{
$oneYearAgo = date('Y-m-d', strtotime('-1 year'));
return $this->query()
->where('sign_date', '<', $oneYearAgo)
->delete();
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\enum\common\EnabledStatus;
use app\model\member\MemberTag;
/**
* 会员标签数据访问对象
*/
class MemberTagDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberTag::class;
}
/**
* 获取标签使用统计
*/
public function getUsageCount(int $tagId): int
{
$relationDao = new MemberTagRelationDao();
return $relationDao->getTagMemberCount($tagId);
}
/**
* 获取启用的标签列表
*/
public function getEnabledTags(): array
{
return $this->query()
->where('enabled', EnabledStatus::ENABLED->value)
->orderBy('sort', 'asc')
->get()
->toArray();
}
/**
* 获取标签详情
*/
public function getTagById(int $tagId): ?MemberTag
{
return $this->find($tagId);
}
/**
* 获取标签列表
*/
public function getTagList(array $params = []): array
{
$query = $this->query();
// 状态筛选
if (isset($params['enabled'])) {
$query->where('enabled', $params['enabled']);
}
// 关键词搜索
if (isset($params['keyword'])) {
$query->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('description', 'like', '%' . $params['keyword'] . '%');
}
return $query->orderBy('sort', 'asc')
->orderBy('id', 'desc')
->get()
->toArray();
}
/**
* 创建或更新标签
*/
public function saveTag(array $data): MemberTag
{
$tagId = $data['id'] ?? 0;
if ($tagId) {
// 更新标签
$tag = $this->find($tagId);
if (!$tag) {
throw new \Exception('标签不存在');
}
} else {
// 创建新标签
$tag = $this->getModel();
$tag->enabled = EnabledStatus::ENABLED->value;
}
// 填充数据
$fillable = [
'name',
'color',
'description',
'sort',
'enabled',
];
foreach ($fillable as $field) {
if (isset($data[$field])) {
$tag->{$field} = $data[$field];
}
}
$tag->save();
return $tag;
}
/**
* 删除标签
*/
public function deleteTag(int $tagId): bool
{
// 检查标签是否被使用
$usageCount = $this->getUsageCount($tagId);
if ($usageCount > 0) {
throw new \Exception('该标签已被使用,无法删除');
}
$tag = $this->find($tagId);
if (!$tag) {
throw new \Exception('标签不存在');
}
return $tag->delete();
}
}
@@ -0,0 +1,26 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace app\dao\member;
use app\model\member\MemberTagPermission;
use core\base\BaseDao;
class MemberTagPermissionDao extends BaseDao
{
protected function setModel(): string
{
return MemberTagPermission::class;
}
}
@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\model\member\MemberTagRelation;
/**
* 会员标签关系数据访问对象
*/
class MemberTagRelationDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberTagRelation::class;
}
/**
* 批量保存标签关系(自动过滤已存在的记录)
*
* @param array $data 标签关系数据 [['member_id' => xx, 'tag_id' => yy], ...]
* @return array 返回结果 ['saved' => int, 'duplicates' => int, 'failed' => int, 'errors' => array]
* @throws \Exception
*/
public function batchSave(array $data): array
{
if (empty($data)) {
return [
'saved' => 0,
'duplicates' => 0,
'failed' => 0,
'errors' => []
];
}
// 验证数据格式
foreach ($data as $index => $item) {
if (!isset($item['member_id']) || !isset($item['tag_id'])) {
throw new \Exception("数据格式错误: 第 {$index} 条记录缺少 member_id 或 tag_id");
}
}
// 检查已存在的记录
$existing = $this->getExistingRelations($data);
// 过滤掉已存在的记录
$newData = $this->filterExistingRelations($data, $existing);
$duplicates = count($data) - count($newData);
if (empty($newData)) {
return [
'saved' => 0,
'duplicates' => $duplicates,
'failed' => 0,
'errors' => []
];
}
try {
// 使用批量插入(性能更好)
$result = $this->getModel()->insert($newData);
if (!$result) {
throw new \Exception('批量插入失败');
}
return [
'saved' => count($newData),
'duplicates' => $duplicates,
'failed' => 0,
'errors' => []
];
} catch (\Throwable $e) {
// 检查是否是唯一键冲突
if ($this->isDuplicateEntryError($e)) {
return [
'saved' => 0,
'duplicates' => count($data),
'failed' => 0,
'errors' => ['部分记录可能已存在']
];
}
throw new \Exception("批量保存失败: " . $e->getMessage(), 0, $e);
}
}
/**
* 获取已存在的关联关系
*
* @param array $data
*
* @return array
* @throws \Exception
*/
protected function getExistingRelations(array $data): array
{
if (empty($data)) {
return [];
}
$query = $this->query();
// 构建查询:WHERE (member_id = x AND tag_id = y) OR (member_id = a AND tag_id = b) ...
$query->where(function($q) use ($data) {
foreach ($data as $item) {
$q->orWhere(function($subQ) use ($item) {
$subQ->where('member_id', $item['member_id'])
->where('tag_id', $item['tag_id']);
});
}
});
return $query->get()->toArray();
}
/**
* 过滤掉已存在的关联关系
*
* @param array $data
* @param array $existing
* @return array
*/
protected function filterExistingRelations(array $data, array $existing): array
{
if (empty($existing)) {
return $data;
}
// 构建已存在记录的 map
$existingMap = [];
foreach ($existing as $record) {
$key = $record['member_id'] . ':' . $record['tag_id'];
$existingMap[$key] = true;
}
// 过滤掉已存在的记录
$newData = [];
foreach ($data as $item) {
$key = $item['member_id'] . ':' . $item['tag_id'];
if (!isset($existingMap[$key])) {
$newData[] = $item;
}
}
return $newData;
}
/**
* 判断是否是重复记录错误
*
* @param \Throwable $e
* @return bool
*/
protected function isDuplicateEntryError(\Throwable $e): bool
{
$message = $e->getMessage();
return str_contains($message, 'Duplicate entry') ||
str_contains($message, '1062') ||
str_contains($message, 'Integrity constraint violation');
}
// /**
// * 添加标签关系
// */
// public function addTagRelation(int $memberId, int $tagId): bool
// {
// $exists = $this->query()
// ->where('member_id', $memberId)
// ->where('tag_id', $tagId)
// ->exists();
//
// if (!$exists) {
// $relation = $this->getModel();
// $relation->member_id = $memberId;
// $relation->tag_id = $tagId;
// return $relation->save();
// }
//
// return true;
// }
//
// /**
// * 移除标签关系
// */
// public function removeTagRelation(int $memberId, int $tagId): bool
// {
// return $this->query()
// ->where('member_id', $memberId)
// ->where('tag_id', $tagId)
// ->delete() > 0;
// }
//
// /**
// * 获取会员的标签ID列表
// */
// public function getMemberTagIds(int $memberId): array
// {
// return $this->query()
// ->where('member_id', $memberId)
// ->pluck('tag_id')
// ->toArray();
// }
//
// /**
// * 获取标签的会员数量
// */
// public function getTagMemberCount(int $tagId): int
// {
// return $this->count(['tag_id' => $tagId]);
// }
}
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use app\enum\common\EnabledStatus;
use core\base\BaseDao;
use app\model\member\MemberThirdParty;
use app\model\member\Member;
/**
* 会员第三方登录DAO
*/
class MemberThirdPartyDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberThirdParty::class;
}
/**
* 绑定第三方账号
*
* @throws \Exception
*/
public function bind(int|string $memberId, int $platform, string $openid, array $data = []): MemberThirdParty
{
// 检查是否已绑定
$thirdParty = $this->findByMemberAndPlatform($memberId, $platform);
if ($thirdParty) {
// 更新绑定信息
foreach ($data as $key => $value) {
if (in_array($key, $thirdParty->fillable)) {
$thirdParty->$key = $value;
}
}
$thirdParty->openid = $openid;
$thirdParty->enabled = EnabledStatus::ENABLED->value;
$thirdParty->save();
return $thirdParty;
}
// 创建新绑定
$thirdParty = new MemberThirdParty();
$thirdParty->member_id = $memberId;
$thirdParty->platform = $platform;
$thirdParty->openid = $openid;
foreach ($data as $key => $value) {
if (in_array($key, $thirdParty->fillable)) {
$thirdParty->$key = $value;
}
}
$thirdParty->enabled = EnabledStatus::ENABLED->value;
$thirdParty->save();
return $thirdParty;
}
/**
* 根据会员ID和平台查找第三方账号
*
* @throws \Exception
*/
public function findByMemberAndPlatform(int|string $memberId, int $platform): ?MemberThirdParty
{
return $this->query()
->where('member_id', $memberId)
->where('platform', $platform)
->first();
}
/**
* 根据第三方账号查找会员
*
* @throws \Exception
*/
public function findMemberByThirdParty(int $platform, string $openid): ?Member
{
$thirdParty = $this->query()
->where('platform', $platform)
->where('openid', $openid)
->where('enabled', EnabledStatus::ENABLED->value)
->with('member')
->first();
/** @var TYPE_NAME $thirdParty */
return $thirdParty?->member;
}
/**
* 根据unionid查找会员
*/
public function findMemberByUnionId(string $unionid): ?Member
{
$thirdParty = $this->query()
->where('unionid', $unionid)
->where('enabled', EnabledStatus::ENABLED->value)
->with('member')
->first();
/** @var TYPE_NAME $thirdParty */
return $thirdParty?->member;
}
/**
* 获取会员的第三方账号列表
*
* @throws \Exception
*/
public function getMemberThirdParties(int|string $memberId): array
{
return $this->query()
->where('member_id', $memberId)
->where('enabled', EnabledStatus::ENABLED->value)
->get()
->toArray();
}
/**
* 解绑第三方账号
*/
public function unbind(MemberThirdParty $thirdParty): bool
{
$thirdParty->enabled = EnabledStatus::DISABLED->value;
return $thirdParty->save();
}
/**
* 检查第三方账号是否已绑定
*/
public function isBound(int $platform, string $openid): bool
{
return $this->query()
->where('platform', $platform)
->where('openid', $openid)
->where('enabled', EnabledStatus::ENABLED->value)
->exists();
}
/**
* 更新第三方登录令牌
*/
public function updateToken(MemberThirdParty $thirdParty, string $accessToken, string $refreshToken = '', int $expiresIn = 0): bool
{
$thirdParty->access_token = $accessToken;
if ($refreshToken) {
$thirdParty->refresh_token = $refreshToken;
}
if ($expiresIn > 0) {
$thirdParty->expires_at = date('Y-m-d H:i:s', time() + $expiresIn);
}
return $thirdParty->save();
}
}
@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\enum\common\EnabledStatus;
use app\enum\member\WithdrawAccountType;
use app\model\member\MemberWithdrawAccount;
/**
* 会员提现账号数据访问对象
*/
class MemberWithdrawAccountDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberWithdrawAccount::class;
}
/**
* 设置默认账号
*/
public function setDefault(MemberWithdrawAccount $account): bool
{
// 先将该会员的所有提现账号设置为非默认
$this->query()
->where('member_id', $account->member_id)
->update(['is_default' => 0]);
// 将当前账号设置为默认
$account->is_default = 1;
return $account->save();
}
/**
* 获取会员默认提现账号
*/
public function getDefaultAccount(int $memberId): ?MemberWithdrawAccount
{
return $this->query()
->where('member_id', $memberId)
->where('is_default', 1)
->where('enabled', EnabledStatus::ENABLED->value)
->first();
}
/**
* 获取会员提现账号列表
*/
public function getMemberAccounts(int $memberId): array
{
return $this->query()
->where('member_id', $memberId)
->where('enabled', EnabledStatus::ENABLED->value)
->orderBy('is_default', 'desc')
->orderBy('updated_at', 'desc')
->get()
->toArray();
}
/**
* 创建或更新提现账号
*/
public function saveAccount(int $memberId, array $data): MemberWithdrawAccount
{
$accountId = $data['id'] ?? 0;
if ($accountId) {
// 更新账号
$account = $this->find($accountId);
if (!$account || $account->member_id != $memberId) {
throw new \Exception('账号不存在或无权限');
}
} else {
// 创建新账号
$account = $this->getModel();
$account->member_id = $memberId;
$account->enabled = EnabledStatus::ENABLED->value;
}
// 填充数据
$fillable = [
'type',
'bank_name',
'account_name',
'account_number',
'branch_name',
];
foreach ($fillable as $field) {
if (isset($data[$field])) {
$account->{$field} = $data[$field];
}
}
// 设置默认账号
if (isset($data['is_default']) && $data['is_default'] == 1) {
// 先将其他账号设置为非默认
$this->query()
->where('member_id', $memberId)
->update(['is_default' => 0]);
$account->is_default = 1;
} elseif (!$accountId) {
// 新账号默认为非默认
$account->is_default = 0;
}
$account->save();
return $account;
}
/**
* 删除提现账号
*/
public function deleteAccount(MemberWithdrawAccount $account): bool
{
// 检查是否有关联的提现记录
$withdrawCount = $account->withdraws()->count();
if ($withdrawCount > 0) {
throw new \Exception('该账号有关联的提现记录,无法删除');
}
$account->enabled = EnabledStatus::DISABLED->value;
return $account->save();
}
/**
* 验证账号数据
*/
public function validateAccount(array $data): array
{
$errors = [];
if (empty($data['type'])) {
$errors[] = '账号类型不能为空';
}
if (empty($data['bank_name'])) {
$errors[] = '银行名称不能为空';
}
if (empty($data['account_name'])) {
$errors[] = '账户姓名不能为空';
}
if (empty($data['account_number'])) {
$errors[] = '账号不能为空';
}
// 根据账号类型进行特殊验证
if (isset($data['type'])) {
switch ($data['type']) {
case WithdrawAccountType::BANK->value:
if (empty($data['branch_name'])) {
$errors[] = '开户行不能为空';
}
break;
case WithdrawAccountType::ALIPAY->value:
// 支付宝账号验证
if (!preg_match('/^1[3-9]\d{9}$|^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/', $data['account_number'])) {
$errors[] = '支付宝账号格式不正确(请输入手机号或邮箱)';
}
break;
case WithdrawAccountType::WECHAT->value:
// 微信账号验证
if (!preg_match('/^[a-zA-Z0-9_-]{6,20}$/', $data['account_number'])) {
$errors[] = '微信账号格式不正确';
}
break;
}
}
return $errors;
}
/**
* 获取会员账号数量
*/
public function getMemberAccountCount(int $memberId): int
{
return $this->count([
'member_id' => $memberId,
'enabled' => EnabledStatus::ENABLED->value
]);
}
/**
* 获取账号使用次数
*/
public function getUsageCount(MemberWithdrawAccount $account): int
{
return $account->withdraws()->count();
}
}
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace app\dao\member;
use core\base\BaseDao;
use app\model\member\MemberWithdraw;
use app\enum\member\WithdrawStatus;
/**
* 会员提现DAO
*/
class MemberWithdrawDao extends BaseDao
{
/**
* 设置模型
*/
protected function setModel(): string
{
return MemberWithdraw::class;
}
/**
* 生成提现订单号
*/
public function generateOrderSn(): string
{
return 'WD' . date('YmdHis') . substr(implode('', array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
/**
* 创建提现记录
*/
public function create(int $memberId, int $accountId, float $amount, float $fee, float $actualAmount, string $bankName, string $bankAccount, string $bankCardholder, string $orderSn, string $remark = ''): MemberWithdraw
{
$withdraw = $this->getModel();
$withdraw->member_id = $memberId;
$withdraw->account_id = $accountId;
$withdraw->amount = $amount;
$withdraw->fee = $fee;
$withdraw->actual_amount = $actualAmount;
$withdraw->status = WithdrawStatus::PENDING->value;
$withdraw->bank_name = $bankName;
$withdraw->bank_account = $bankAccount;
$withdraw->bank_cardholder = $bankCardholder;
$withdraw->order_sn = $orderSn;
$withdraw->remark = $remark;
$withdraw->save();
return $withdraw;
}
/**
* 根据订单号查找提现记录
*/
public function findByOrderSn(string $orderSn): ?MemberWithdraw
{
return $this->query()
->where('order_sn', $orderSn)
->first();
}
/**
* 更新提现状态
*/
public function updateStatus(MemberWithdraw $withdraw, int $status, string $auditRemark = ''): bool
{
$withdraw->status = $status;
if ($auditRemark) {
$withdraw->audit_remark = $auditRemark;
}
$withdraw->audit_at = date('Y-m-d H:i:s');
return $withdraw->save();
}
/**
* 获取会员提现列表
*/
public function getMemberWithdraws(int $memberId, array $params = []): array
{
$query = $this->query()->where('member_id', $memberId);
// 状态筛选
if (isset($params['status'])) {
$query->where('status', $params['status']);
}
// 时间范围筛选
if (isset($params['start_date']) && isset($params['end_date'])) {
$query->whereBetween('created_at', [
$params['start_date'] . ' 00:00:00',
$params['end_date'] . ' 23:59:59'
]);
}
// 分页参数
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 10;
return $query->orderBy('created_at', 'desc')
->paginate($limit, ['*'], 'page', $page)
->toArray();
}
/**
* 获取提现统计
*/
public function getWithdrawStats(int $memberId): array
{
$totalApply = $this->count(['member_id' => $memberId]);
$totalSuccess = $this->count([
'member_id' => $memberId,
'status' => WithdrawStatus::COMPLETED->value
]);
$totalAmount = $this->query()
->where('member_id', $memberId)
->where('status', WithdrawStatus::COMPLETED->value)
->sum('actual_amount');
return [
'total_apply' => $totalApply,
'total_success' => $totalSuccess,
'total_amount' => $totalAmount,
];
}
/**
* 获取待审核提现列表
*/
public function getPendingWithdraws(array $params = []): array
{
$query = $this->query()
->where('status', WithdrawStatus::PENDING->value);
// 会员ID筛选
if (isset($params['member_id'])) {
$query->where('member_id', $params['member_id']);
}
// 分页参数
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 10;
return $query->orderBy('created_at', 'desc')
->with('member')
->paginate($limit, ['*'], 'page', $page)
->toArray();
}
/**
* 获取提现总数
*/
public function getCount(array $params = []): int
{
$query = $this->query();
// 会员ID筛选
if (isset($params['member_id'])) {
$query->where('member_id', $params['member_id']);
}
// 状态筛选
if (isset($params['status'])) {
$query->where('status', $params['status']);
}
return $query->count();
}
}