This commit is contained in:
2025-08-13 14:12:31 +08:00
parent c95616561f
commit 34da927bc9
10 changed files with 2021 additions and 23 deletions

135
app/DbCoroutineContext.php Normal file
View File

@@ -0,0 +1,135 @@
<?php
namespace app;
class DbCoroutineContext
{
// 协程隔离数据
protected static array $data = [];
protected static array $list = [];
// 普通环境全局数据
protected static array $globalData = [];
protected static array $globalList = [];
protected static function getCid(): ?int
{
if (class_exists('\Swoole\Coroutine')) {
$cid = \Swoole\Coroutine::getCid();
if ($cid >= 0) {
return $cid;
}
}
return null;
}
// put 存数据到 list
public static function put(mixed $value): void
{
$val = self::get('startTrans');
if (!empty($val) && $val === true) {
$value->startTrans();
}
$cid = self::getCid();
if ($cid !== null) {
self::$list[$cid][] = $value;
} else {
self::$globalList[] = $value;
}
}
// set 设置 key-value
public static function set(string $key, mixed $value): void
{
$cid = self::getCid();
if ($cid !== null) {
self::$data[$cid][$key] = $value;
} else {
self::$globalData[$key] = $value;
}
}
// get 获取 key-value
public static function get(string $key, mixed $default = null): mixed
{
$cid = self::getCid();
if ($cid !== null) {
return self::$data[$cid][$key] ?? $default;
} else {
return self::$globalData[$key] ?? $default;
}
}
// 获取 list
public static function getList(): array
{
$cid = self::getCid();
if ($cid !== null) {
return self::$list[$cid] ?? [];
} else {
return self::$globalList;
}
}
// 删除指定 key
public static function delete(string $key): void
{
$cid = self::getCid();
if ($cid !== null) {
unset(self::$data[$cid][$key]);
} else {
unset(self::$globalData[$key]);
}
}
public static function clearList()
{
foreach (self::getList() as $conn) {
try {
$conn->query('SELECT 1');
$conn->close();
// 连接正常
} catch (\Exception $e) {
}
}
$cid = self::getCid();
if ($cid !== null) {
unset(self::$list[$cid]);
} else {
self::$globalList = [];
}
}
public static function rollback()
{
foreach (self::getList() as $conn) {
$conn->rollback();
}
}
// 清理当前上下文
public static function clear(): void
{
foreach (self::getList() as $conn) {
try {
$conn->query('SELECT 1');
$conn->close();
// 连接正常
} catch (\Exception $e) {
}
}
$cid = self::getCid();
if ($cid !== null) {
unset(self::$data[$cid], self::$list[$cid]);
} else {
self::$globalData = [];
self::$globalList = [];
}
}
public static function commit()
{
foreach (self::getList() as $conn) {
$conn->commit();
}
}
}

View File

@@ -2,31 +2,192 @@
namespace app\controller;
use app\api\model\CourseCollect;
use app\api\model\CourseDetails;
use app\api\model\TbUser;
use app\model\DatabaseRoute;
use app\model\Test;
use support\Request;
use think\facade\Db;
use think\facade\Log;
class IndexController
{
public function index(Request $request)
{
return <<<EOF
<style>
* {
padding: 0;
margin: 0;
}
iframe {
border: none;
overflow: scroll;
}
</style>
<iframe
src="https://www.workerman.net/wellcome"
width="100%"
height="100%"
allow="*"
sandbox="allow-scripts allow-same-origin"
></iframe>
EOF;
$get['courseId'] = $course_id = '1877654905222135809';
$user['user_id'] = $user_id = '14240';
$user = DatabaseRoute::getDb('tb_user', $user_id)->find();
try {
if(empty($get['courseId'])) {
return json('参数不完整');
}
$courseId = $get['courseId'];
// 获取短剧详情
$dd_b = Db::connect('duanju_slave');
$db_name = $dd_b->name('course');
$bean = $db_name->where(['course_id' => $courseId])->find();
if(!$bean) {
return json('短剧不存在');
}
$courseCollect = DatabaseRoute::getDb('course_collect', $user_id)
->where(['course_id' => $course_id])
->where(['user_id' => $user_id])
->where(['classify' => 3])
->limit(1)
->find();
// 是否追剧
$collect = DatabaseRoute::getDb('course_collect', $user_id)
->where(['course_id' => $course_id])
->where(['classify' => 1])
->limit(1)
->find();
$db = Db::connect(config('think-orm.search_library'));
$userVip = $db->name('user_vip')->where(['user_id' => $user['user_id']])->find();
if ($userVip) {
$user['member'] = $userVip['is_vip'];
$user['end_time'] = $userVip['end_time'];
}
$userInfo = $user;
if (!empty($userInfo['member']) && $userInfo['member'] == 2) {
$isVip = true;
}else{
$isVip = false;
}
// 查询用户是否购买了整集
$courseUser = DatabaseRoute::getDb('course_user', $user_id)
->where(['course_id' => $course_id])
->where(['classify' => 1])
->find();
// 每天购买超过上限,获得免费时间段资格
$freeWatch = Test::checkFreeWatchPayCount($user['user_id']);
$startSort = 0;
$endSort = 5;
$dn_course_details = DatabaseRoute::getDb('course_details', ['course_id' => $courseId]);
$sort = null;
if (is_null($sort)) {
if (!empty($courseCollect)) {
$courseDetails = $dn_course_details->field('sort')
->where('course_details_id', $courseCollect['course_details_id'])
->limit(1)
->find();
$sort = $courseDetails['sort'];
}
}
if ($freeWatch || !empty($courseUser)) {
$courseDetailsSetVos = Test::courseSets($courseId, 2, null);
} else {
$courseDetailsSetVos = Test::courseSets($courseId, 1, $bean['wholesale_price']);
}
// 调整集数范围
if (!is_null($sort) && $sort > 2) {
$startSort = $sort - 3;
$endSort = $sort + 3;
if (count($courseDetailsSetVos) < $endSort) {
$startSort = count($courseDetailsSetVos) - 5;
$endSort = count($courseDetailsSetVos) + 1;
}
}
// 已购买剧集ID集合
$detailsId = [];
if (!$freeWatch) {
$det_db = Db::connect(DatabaseRoute::getConnection('course_user', ['user_id' => $user['user_id']]));
$detailsId = $det_db->name('course_user')->where(['course_id' => $courseId, 'classify' => 2])->column('course_details_id');
$det_db->close();
$detailsId = array_flip(array_flip($detailsId)); // 去重
}
// 处理剧集列表
$current = null;
foreach ($courseDetailsSetVos as &$s) {
$s['wholesalePrice'] = (int) $s['wholesalePrice'];
// 当前播放集
if (!empty($courseCollect) && $s['courseDetailsId'] == $courseCollect['course_details_id']) {
$s['current'] = 1;
$current = &$s;
}
// 非免费用户的权限控制
if (
!$freeWatch &&
$s['sort'] > 3 &&
(empty($detailsId) || !in_array($s['courseDetailsId'], $detailsId)) &&
empty($courseUser) &&
!$isVip
) {
$s['videoUrl'] = null;
}
// 检查是否已点赞
if ($s['sort'] > $startSort && $s['sort'] < $endSort) {
$isGood_db = Db::connect(DatabaseRoute::getConnection('course_collect', ['user_id' => $user['user_id']]));
$isGood = $isGood_db->name('course_collect')
->where('course_details_id', $s['courseDetailsId'])
->where('classify', 2)
->limit(1)
->count();
$isGood_db->close();
$s['isGood'] = empty($isGood) || $isGood == 0 ? 0 : 1;
}
}
// 如果没有当前播放集,默认第一集
if (empty($current) && !empty($courseDetailsSetVos)) {
$courseDetailsSetVos[0]['current'] = 1;
$current = &$courseDetailsSetVos[0];
}
Test::setCourseView($bean);
$price = ($freeWatch ? 0 : ($bean['price'] ?? 0));
$price = bccomp($price, '0', 2) <= 0 ? 0 : $price;
// 返回结果
$map = [
'current' => $current,
'price' => $price,
'title' => $bean['title'],
'collect' => empty($collect) || $collect == 0 ? 0 : 1,
'list' => $courseDetailsSetVos
];
return json($map);
} catch (\Exception $e) {
return json($e->getMessage());
}
}
public function view(Request $request)

516
app/model/DatabaseRoute.php Normal file
View File

@@ -0,0 +1,516 @@
<?php
namespace app\model;
use app\DbCoroutineContext;
use think\Collection;
use think\db\Connection;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Log;
class DatabaseRoute
{
/**
* 跨所有分库分页(自动 count自动追加 LIMIT 和 ORDER BY
*
* @param \Closure $sqlBuilder 生成基础 SQL 的闭包函数(不包含 LIMIT
* @param int $page 当前页码
* @param int $pageSize 每页条数
* @param string|null $orderCol 排序字段(全局排序使用)
* @return array
*/
public static function paginateAllDbBySqlAutoCount(
\Closure $sqlBuilder,
int $page = 1,
int $pageSize = 20,
string $orderCol = null,
int $count = null,
): array
{
$dbMap = config('database.db_map');
$counts = [];
$total = 0;
if ($count === null) {
// 1. 自动 count 分库总数
$dbList = DatabaseRoute::getAllSelectDb();
foreach ($dbMap as $connName) {
if (in_array($connName, $dbList) ) {
$baseSql = call_user_func($sqlBuilder, $connName);
$countSql = self::buildCountSql($baseSql);
$count = Db::connect($connName)->query($countSql)[0]['count'] ?? 0;
$counts[$connName] = $count;
$total += $count;
}
}
}
if ($total === 0) {
return [
'list' => [],
'totalCount' => 0,
'totalPage' => 0,
'currPage' => $page,
'pageSize' => $pageSize,
];
}
$offset = ($page - 1) * $pageSize;
if ($offset >= $total) {
return [
'list' => [],
'totalCount' => $total,
'totalPage' => (int)ceil($total / $pageSize),
'currPage' => $page,
'pageSize' => $pageSize,
];
}
$limit = $pageSize;
$allRows = [];
$skip = $offset;
// 2. 分库分页查询
foreach ($counts as $connName => $count) {
if ($count <= 0) continue;
if ($skip >= $count) {
$skip -= $count;
continue;
}
$localOffset = $skip;
$localLimit = min($count - $localOffset, $limit);
$baseSql = call_user_func($sqlBuilder, $connName);
$sql = rtrim($baseSql, ';');
if ($orderCol) {
$sql .= " ORDER BY {$orderCol} DESC";
}
$sql .= " LIMIT {$localOffset}, {$localLimit}";
$rows = Db::connect($connName)->query($sql);
$allRows = array_merge($allRows, $rows);
$limit -= $localLimit;
$skip = 0;
if ($limit <= 0) break;
}
// 3. 全局排序(若指定排序字段)
if ($orderCol) {
usort($allRows, function ($a, $b) use ($orderCol) {
return strtotime($b[$orderCol]) <=> strtotime($a[$orderCol]);
});
}
// 4. 返回结构
return [
'list' => $allRows,
'totalCount' => $total,
'totalPage' => (int)ceil($total / $pageSize),
'currPage' => $page,
'pageSize' => $pageSize,
];
}
/**
* 从 SELECT SQL 自动构建对应的 COUNT SQL
*
* @param string $sql
* @return string
*/
private static function buildCountSql(string $sql): string
{
$sql = preg_replace('/\s+ORDER\s+BY\s+.+?$/i', '', $sql);
$sql = preg_replace('/\s+LIMIT\s+\d+(\s*,\s*\d+)?$/i', '', $sql);
if (preg_match('/FROM\s+(.*)/i', $sql, $matches)) {
return 'SELECT COUNT(*) AS count FROM ' . $matches[1];
}
throw new \RuntimeException("无法从 SQL 自动构建 COUNT 查询,请检查 SQL 是否规范");
}
public static function paginateDb(
string $table,
\Closure $builder,
int $page = 1,
int $pageSize = 20,
string|array $keyInfo = null,
$isRecords = false
) {
// 构建基础查询
if ($keyInfo) {
$query = self::getDb($table, $keyInfo);
}else{
$query = Db::table($table);
}
$builder($query); // 应用外部闭包设置查询条件,如 where/order 等
// 获取总数
$totalCount = (clone $query)->count();
// 计算总页数
$totalPage = (int) ceil($totalCount / $pageSize);
$currPage = max(1, min($page, $totalPage));
// 获取分页数据
$list = $query
->page($currPage, $pageSize)
->select()
->toArray();
$query->getConnection()->close();
return [
'totalCount' => $totalCount,
'pageSize' => $pageSize,
'totalPage' => $totalPage,
'currPage' => $currPage,
$isRecords ? 'records' : 'list' => convertToCamelCase($list),
];
}
/**
* 跨所有从库分页(支持游标分页和传统页码分页)
* @param string $table 表名
* @param \Closure $builder 查询构造器
* @param int|null $page 当前页null 表示使用游标分页)
* @param int $pageSize 每页条数
* @param string|null $lastCreateTime 游标分页的游标值create_time
* @return array [list, total|nextCursor, hasMore]
*/
public static function paginateAllDb(
string $table,
\Closure $builder,
int $page = 1,
int $pageSize = 20,
string $orderCol = 'create_time',
string $modelOrderCol = null,
bool $isRecords = false
): array
{
$dbMap = config('database.db_map');
$counts = [];
$total = 0;
// 1. 统计每个分库总数
$dbList = DatabaseRoute::getAllSelectDb();
foreach ($dbMap as $connName) {
if (in_array($connName, $dbList) ) {
$query = Db::connect($connName)->name($table);
$query = call_user_func($builder, $query);
// 不要order避免报错
$query->removeOption('order');
$count = $query->count();
$query->getConnection()->close();
$counts[$connName] = $count;
$total += $count;
}
}
if ($total === 0) {
return [
$isRecords ? 'records' : 'list' => [],
'totalCount' => 0,
'totalPage' => 0,
'currPage' => $page,
'pageSize' => $pageSize,
];
}
$offset = ($page - 1) * $pageSize;
if ($offset >= $total) {
// 超出总数,返回空数据
return [
$isRecords ? 'records' : 'list' => [],
'totalCount' => $total,
'totalPage' => (int)ceil($total / $pageSize),
'currPage' => $page,
'pageSize' => $pageSize,
];
}
$limit = $pageSize;
$allRows = [];
// 2. 计算每个库需要查询的偏移和条数
$skip = $offset; // 还需要跳过多少条
foreach ($counts as $connName => $count) {
if ($count <= 0) continue;
if ($skip >= $count) {
// 跳过整个分库
$skip -= $count;
continue;
}
// 本库要查询的起始 offset
$localOffset = $skip;
// 本库剩余条数
$localLimit = min($count - $localOffset, $limit);
// 查询数据
$query = Db::connect($connName)->name($table);
$query = call_user_func($builder, $query);
$rows = $query->order($orderCol, 'desc')
->limit($localOffset, $localLimit)
->select()
->toArray();
$query->getConnection()->close();
$allRows = array_merge($allRows, $rows);
$limit -= $localLimit; // 剩余要查询的条数
$skip = 0; // 之后库不再跳过
if ($limit <= 0) break; // 已查够
}
// 3. 全局排序(降序)
if ($modelOrderCol) {
usort($allRows, function ($a, $b) use ($modelOrderCol) {
return strtotime($b[$modelOrderCol]) <=> strtotime($a[$modelOrderCol]);
});
}
// 4. 返回分页数据
return [
$isRecords ? 'records' : 'list' => $allRows,
'totalCount' => $total,
'totalPage' => (int)ceil($total / $pageSize),
'currPage' => $page,
'pageSize' => $pageSize,
];
}
/**
* 对所有从库执行事务XA 分布式模拟,非真实两阶段提交)
* @param \Closure $callback
* @return mixed
* @throws \Throwable
*/
public static function transactionXa(\Closure $callback)
{
DbCoroutineContext::set('startTrans', true);
try {
// 执行用户操作(传入事务闭包)
$result = call_user_func($callback);
// 提交所有连接
DbCoroutineContext::commit();
return $result;
} catch (\Throwable $e) {
// 回滚所有连接
DbCoroutineContext::rollback();
Log::error("transactionXa 全部回滚:" . $e->getMessage());
Log::info('错误信息'.$e->getMessage().'具体信息'.$e->getTraceAsString());
throw $e;
}finally {
DbCoroutineContext::clearList();
DbCoroutineContext::set('startTrans', false);
}
}
public static function getMasterDb($table, $isWrite = false)
{
$conn = Db::connect();
if ($isWrite) {
DbCoroutineContext::put($conn);
}
return $conn->name($table);
}
/**
* 根据表和分库值获取db
* @param $table string 表名
* @param $keyInfo string | array
*/
public static function getDb($table, $keyInfo, $isWrite = false, $isUpdate = false, $addWhere = true)
{
if (is_array($keyInfo)) {
$con = Db::connect(DatabaseRoute::getConnection($table, $keyInfo, $isWrite));
if ($isWrite) {
DbCoroutineContext::put($con);
}
$model = $con->name($table);
if ($addWhere && (!$isWrite || $isUpdate)) {
$model->where($keyInfo);
}
} else {
$con = Db::connect(DatabaseRoute::getConnection($table, ['user_id' => $keyInfo], $isWrite));
if ($isWrite) {
DbCoroutineContext::put($con);
}
$model = $con->name($table);
if ($addWhere && (!$isWrite || $isUpdate)) {
$model->where([
'user_id' => $keyInfo
]);
}
}
return $model;
}
/**
* 跨所有分库直接删除(危险操作,务必确认条件准确)
* @param string $table 表名
* @param \Closure $builder 条件构造器,如 function($query) { return $query->where('xxx', xxx); }
* @return int 删除的总记录数
*/
public static function deleteAllDbDirect(string $table, \Closure $builder): int
{
$dbMap = config('database.db_master_map');
$totalDeleted = 0;
foreach ($dbMap as $connName) {
if ($connName === 'duanju_master') continue;
$query = Db::connect($connName)->name($table);
$query = call_user_func($builder, $query);
$deleted = $query->delete();
$query->getConnection()->close();
$totalDeleted += $deleted;
}
return $totalDeleted;
}
public static function getAllDbData($table, \Closure $builder)
{
return new class($table, $builder) {
protected $table;
protected $builder;
protected $dbMap;
protected $masterDbMap;
public function __construct($table, $builder)
{
$this->table = $table;
$this->builder = $builder;
$this->dbMap = config('database.db_map');
$this->masterDbMap = config('database.db_master_map');
}
public function __call($method, $args)
{
$finalResult = null;
try {
if ($method == 'insert') {
throw new \RuntimeException("不支持insert");
}
// insert 和 update 使用主库
if ($method == 'update' || $method == 'delete') {
foreach ($this->masterDbMap as $dbname) {
if ($dbname !== 'duanju_master') {
$query = Db::connect($dbname)->name($this->table);
$query = call_user_func($this->builder, $query);
if (method_exists($query, $method)) {
$result = call_user_func_array([$query, $method], $args);
$finalResult = $result;
$query->getConnection()->close();
// find 返回 nullselect 返回空数组count 返回数字
if ($result || $result === 0) {
return $result;
}
}else {
$query->getConnection()->close();
}
}
}
return $finalResult;
}else{
$dbList = DatabaseRoute::getAllSelectDb();
foreach ($this->dbMap as $dbname) {
if (in_array($dbname, $dbList) ) {
$query = Db::connect($dbname)->name($this->table);
$query = call_user_func($this->builder, $query);
if (method_exists($query, $method)) {
$result = call_user_func_array([$query, $method], $args);
// find 返回 nullselect 返回空数组count 返回数字
$finalResult = $result;
$query->getConnection()->close();
if ($result instanceof Collection) {
if (!$result->isEmpty()) {
return $result;
}
}elseif (is_int($result) && $result != 0) {
return $result;
} else if ($result) {
return $result;
}
}else {
$query->getConnection()->close();
}
}
}
}
}catch (\Throwable $e) {
Log::error("错误信息".$e->getMessage());
throw $e;
}
return $finalResult;
}
};
}
public static function getAllSelectDb()
{
$index = self::getIndex();
return ["duanju$index-0_slave", "duanju$index-1_slave", "duanju$index-2_slave", "duanju$index-3_slave", "duanju$index-4_slave"];
}
private static function getIndex()
{
$INDEX = \cache('db_index');
if ($INDEX == null) {
$INDEX = 0;
}
$INDEX = ++$INDEX % 4;
\cache('db_index', $INDEX);
return $INDEX;
}
/**
* 获取数据库连接
* @param string $tableName 表名
* @param array $data 数据
* @param bool $isWrite 是否为写操作
* @return Connection
*/
public static function getConnection($table, $data = [], $isWrite = false)
{
$routeConfig = config('think-orm.route');
$keyField = strpos($table, 'user') !== false || in_array($table, [
'orders', 'course_collect', 'pay_details', 'disc_spinning_record',
'cash_out', 'course_user', 'tb_user', 'task_center_record',
'user_money', 'user_sign_record', 'invite_achievement',
'invite_money', 'user_info', 'sys_user', 'user_money_details', 'sys_user_money_details'
]) ? 'user_id' : 'course_id';
if (!isset($data[$keyField])) {
Log::warning("分库警告: 表={$table}, 数据中缺少 {$keyField} 字段");
return $isWrite ? 'duanju_master' : 'duanju_slave';
}
$index = abs($data[$keyField] % 5);
$connectionTemplate = $isWrite
? $routeConfig[$table]['master']
: $routeConfig[$table]['slave'];
$connectionName = str_replace("{\${$keyField}%5}", $index, $connectionTemplate);
return $connectionName;
}
}

View File

@@ -2,7 +2,8 @@
namespace app\model;
use support\Model;
use think\model;
use think\facade\Db;
class Test extends Model
{
@@ -26,4 +27,84 @@ class Test extends Model
* @var bool
*/
public $timestamps = false;
public static function checkFreeWatchPayCount($userId)
{
$count = DatabaseRoute::getDb('orders', $userId)->where([
'status' => 1,
'pay_way' => 9,
['create_time', '>', date('Y-m-d 00:00:00')],
])->count();
$needCount = Db::name('common_info')->where([
'type' => 916
])->find();
$freeTime = Db::name('common_info')->where([
'type' => 917
])->find();
if (!$needCount || !$freeTime) {
return false;
}
if ($count >= intval($needCount['value'])) {
$isExpire = false;
}else{
$isExpire = true;
}
return !$isExpire;
}
public static function courseSets($courseId, $isPrice, $wholesalePrice)
{
$db = Db::connect(DatabaseRoute::getConnection('course_details', ['course_id' => $courseId]));
$courseDetailsSetVos = $db->name('course_details')
->alias('c')
->field([
'c.course_id' => 'courseId',
'c.course_details_id' => 'courseDetailsId',
'c.course_details_name' => 'courseDetailsName',
'c.video_url' => 'videoUrl',
'c.price' => 'price',
'c.sort' => 'sort',
'c.is_price' => 'isPrice',
'c.title_img' => 'titleImg',
'c.good_num' => 'goodNum',
])
->where('c.course_id', $courseId)
->order('c.sort', 'asc')
->select()
->toArray();
foreach ($courseDetailsSetVos as $k => &$v) {
$v['courseId'] = (string) $v['courseId'];
$v['courseDetailsId'] = (string) $v['courseDetailsId'];
if(empty($wholesalePrice)) {
$v['wholesalePrice'] = 0;
}else {
$v['wholesalePrice'] = $wholesalePrice;
}
if($isPrice != 1) {
$v['isPrice'] = 2;
}
}
return $courseDetailsSetVos;
}
public static function setCourseView($course)
{
// 1. 更新总播放量
if (empty($course['view_counts'])) {
$viewCounts = 1;
} else {
$viewCounts = $course['view_counts'] + 1;
}
$db_name = Db::connect(config('think-orm.z_library'))->name('course');
// 4. 执行数据库更新
$db_name->where(['course_id' => $course['course_id']])->update([
'view_counts' => $viewCounts,
'week_view' => 999
]);
}
}