refactor(member, auth): streamline product info retrieval and enhance dashboard statistics

- Updated `getProductInfo` method in `Member` model to use a unified model for fetching product data.
- Refactored `logLogin` method to utilize `MemberLoginLog` for logging login attempts.
- Introduced `getDashboardStats` method in `AuthService` to gather user-specific statistics, including remaining quotas and counts of authorized accounts, published tasks, and works.
- Added new database configuration for Douyin business statistics.
This commit is contained in:
nepiedg
2026-04-02 07:09:29 +00:00
parent 7dbd84ea98
commit 6b46767d86
8 changed files with 281 additions and 19 deletions
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace app\api\model;
use think\Model;
/**
* 发布任务模型(对应 dy_video_cron 表)
*
* 首页“发布任务”统计与 acgpmw 一致,只排除 status=3 的记录。
*/
class DyVideoCron extends Model
{
protected $connection = 'dbmember';
protected $name = 'dy_video_cron';
protected $pk = 'id';
protected $autoWriteTimestamp = false;
/**
* 统计当前用户的有效发布任务数量。
*
* @param int $userid 用户ID
* @return int
*/
public static function countActiveByUserId(int $userid): int
{
return (int) self::where('userid', $userid)
->where('status', '<>', 3)
->count();
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace app\api\model;
use think\Model;
/**
* 授权账号模型(对应 dy_video_user 表)
*
* 首页“授权账号”卡片按 acgpmw 首页逻辑统计未禁用账号数。
*/
class DyVideoUser extends Model
{
protected $connection = 'dbmember';
protected $name = 'dy_video_user';
protected $pk = 'id';
protected $autoWriteTimestamp = false;
/**
* 统计当前用户可用的授权账号数量。
*
* @param int $userid 用户ID
* @return int
*/
public static function countActiveByUserId(int $userid): int
{
return (int) self::where('userid', $userid)
->where('disabled', 0)
->count();
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace app\api\model;
use think\Model;
/**
* 抖音发布日志模型(对应分表 dys_video_log_{userid % 1000}
*
* 该表在 douying 库中按用户取模分表,首页“发布作品”统计需要按原系统规则动态定位表名。
*/
class DysVideoLog extends Model
{
protected $connection = 'dbdouying';
// 仅作为默认占位,真正查询时会按用户ID动态切换分表名。
protected $name = 'dys_video_log_0';
protected $pk = 'id';
protected $autoWriteTimestamp = false;
/**
* 统计当前用户已发布作品数量。
*
* 此处按 acgpmw 首页逻辑对齐,仅统计 status<=1 的发布记录。
*
* @param int $userid 用户ID
* @return int
*/
public static function countPublishedByUserId(int $userid): int
{
$tableName = self::resolveTableName($userid);
return (int) (new self())
->db()
->name($tableName)
->where('userid', $userid)
->where('status', '<=', 1)
->count();
}
/**
* 根据用户ID计算分表名。
*
* @param int $userid 用户ID
* @return string
*/
private static function resolveTableName(int $userid): string
{
return sprintf('dys_video_log_%d', $userid % 1000);
}
}
+5 -10
View File
@@ -95,13 +95,10 @@ class Member extends Model
*/
public function getProductInfo(): ?array
{
// 从主库获取套餐信息
$product = \think\facade\Db::connect('dbbiz')
->name('product_list')
->where('v_type', $this->v_type)
->find();
// 套餐配置改由模型统一封装,避免业务层散落裸表查询。
$product = ProductList::findByVType((int) $this->v_type);
return $product;
return $product ? $product->toArray() : null;
}
/**
@@ -126,9 +123,7 @@ class Member extends Model
public function logLogin(bool $success, string $loginType = 'password'): void
{
try {
\think\facade\Db::connect('dbmember')
->name('member_login_log')
->insert([
MemberLoginLog::recordLogin([
'userid' => $this->userid,
'ip' => request()->ip(),
'time' => time(),
@@ -138,7 +133,7 @@ class Member extends Model
'adminid' => 0,
'v_type' => $this->v_type ?? 0,
]);
} catch (\Exception $e) {
} catch (\Throwable $e) {
// 日志记录失败不影响登录
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace app\api\model;
use think\Model;
/**
* 会员登录日志模型(对应 member_login_log 表)
*
* 这里封装登录日志写入,避免业务层直接拼接表名。
*/
class MemberLoginLog extends Model
{
protected $connection = 'dbmember';
protected $name = 'member_login_log';
protected $autoWriteTimestamp = false;
/**
* 记录一次登录结果。
*
* @param array $payload 登录日志字段
* @return void
*/
public static function recordLogin(array $payload): void
{
$model = new self();
$model->save($payload);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace app\api\model;
use think\Model;
/**
* 套餐模型(对应业务库 product_list 表)
*
* 该模型用于读取会员套餐配置,供登录态与首页工作台展示使用。
*/
class ProductList extends Model
{
// 套餐配置存放在业务库。
protected $connection = 'dbbiz';
// 对齐原系统 product_list 表。
protected $name = 'product_list';
// 当前项目仅做读取,不依赖自动时间戳。
protected $autoWriteTimestamp = false;
/**
* 按套餐类型获取套餐信息。
*
* @param int $vType 套餐类型
* @return self|null
*/
public static function findByVType(int $vType): ?self
{
return self::where('v_type', $vType)->find();
}
}
+56
View File
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace app\api\service;
use app\api\common\Jwt;
use app\api\model\DyVideoCron;
use app\api\model\DyVideoUser;
use app\api\model\DysVideoLog;
use app\api\model\Member;
/**
@@ -155,6 +158,7 @@ class AuthService
// 获取套餐信息
$productInfo = $member->getProductInfo();
$dashboardStats = $this->getDashboardStats($member, $productInfo);
return [
'userid' => $member->userid,
@@ -163,14 +167,66 @@ class AuthService
'endtime' => $member->endtime,
'formtypeid' => $member->formtypeid,
'disabled' => $member->disabled,
'video_num' => $member->video_num,
'sp_num' => $member->sp_num,
'product' => $productInfo ? [
'v_type' => $productInfo['v_type'] ?? null,
'nameu' => $productInfo['nameu'] ?? '',
'video_num' => $productInfo['video_num'] ?? 0,
'account_num' => $productInfo['account_num'] ?? 0,
] : null,
'stats' => $dashboardStats,
];
}
/**
* 获取首页统计数据。
*
* 此处按 acgpmw `dyai/controller/index.php::right()` 的首页语义对齐:
* - 账户剩余条数:优先展示用户余额 `member.video_num`
* - 授权账号:统计 `dy_video_user`
* - 发布任务:统计 `dy_video_cron`
* - 发布作品:统计抖音日志分表 `dys_video_log_{userid % 1000}`
*
* 查询失败时返回 null,避免前端把异常误展示为 0。
*
* @param Member $member 当前登录用户
* @param array|null $productInfo 当前套餐信息
* @return array
*/
private function getDashboardStats(Member $member, ?array $productInfo = null): array
{
$isUnlimitedQuota = $productInfo && (int) ($productInfo['video_num'] ?? 0) < 0;
$stats = [
'remaining_quota' => $isUnlimitedQuota ? null : (int) ($member->video_num ?? 0),
'remaining_quota_unlimited' => $isUnlimitedQuota,
'published_works_count' => null,
'authorized_account_count' => null,
'published_task_count' => null,
];
try {
$stats['authorized_account_count'] = DyVideoUser::countActiveByUserId((int) $member->userid);
} catch (\Throwable $exception) {
// 统计接口需要尽量稳健,单项查询失败时不阻断登录信息返回。
}
try {
$stats['published_task_count'] = DyVideoCron::countActiveByUserId((int) $member->userid);
} catch (\Throwable $exception) {
// 这里与 acgpmw 一样只排除 status=3 的任务,其余状态均计入首页统计。
}
try {
$stats['published_works_count'] = DysVideoLog::countPublishedByUserId((int) $member->userid);
} catch (\Throwable $exception) {
// 发布作品日志使用分表存储,查询失败时前端按“待接入”兜底展示。
}
return $stats;
}
/**
* 修改密码
* @param int $userid
+21
View File
@@ -62,6 +62,27 @@ return [
'fields_cache' => false,
],
// 抖音业务数据库(用于首页发布作品统计分表)
'dbdouying' => [
'type' => 'mysql',
'hostname' => env('DB_DOUYING_HOSTNAME', 'rm-m5e6936bb24oj5272co.mysql.rds.aliyuncs.com'),
'database' => env('DB_DOUYING_DATABASE', 'douying'),
'username' => env('DB_DOUYING_USERNAME', 'contenttpl'),
'password' => env('DB_DOUYING_PASSWORD', 'QXYxgg123!@#q'),
'hostport' => env('DB_DOUYING_HOSTPORT', '3306'),
'params' => [],
'charset' => env('DB_DOUYING_CHARSET', 'utf8'),
'prefix' => '',
'deploy' => 0,
'rw_separate' => false,
'master_num' => 1,
'slave_no' => '',
'fields_strict' => false,
'break_reconnect' => false,
'trigger_sql' => env('APP_DEBUG', true),
'fields_cache' => false,
],
// 后台管理数据库
'dbxgg' => [
'type' => 'mysql',