新增:双色球彩票系统后端基础(阶段一)

📦 数据库
  - lottery_issues(期次表)
  - lottery_tickets(购票记录表)
  - lottery_pool_logs(奖池流水表,透明展示)

🔩 核心组件
  - LotteryIssue / LotteryTicket / LotteryPoolLog 完整 Model
  - LotteryService:购票/机选/开奖/奖池派发/滚存/超级期预热/公屏广播
  - LotteryController:current/buy/quickPick/history/my 五个接口
  - DrawLotteryJob(每日定时开奖)/ OpenLotteryIssueJob(初始化首期)

💰 货币日志
  - CurrencySource 新增 LOTTERY_BUY / LOTTERY_WIN
  - 所有金币变动均通过 UserCurrencyService::change() 记录流水

🗓️ 调度器
  - 每分钟检查停售/开奖时机
  - 每日 18:00 超级期预热广播

🔧 配置
  - GameConfigSeeder 追加 lottery 默认配置(默认关闭)
  - /games/enabled 接口追加 lottery 开关状态
  - 新增 /lottery/* 路由组(auth 保护)
This commit is contained in:
2026-03-04 15:38:02 +08:00
parent b30be5c053
commit 27371fe321
15 changed files with 1394 additions and 0 deletions

164
app/Models/LotteryIssue.php Normal file
View File

@@ -0,0 +1,164 @@
<?php
/**
* 文件功能:双色球期次 Model
*
* 管理每期彩票的生命周期open购票中 closed停售 settled已开奖
* 提供静态查询方法、奖级判定辅助及号码排序工具。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class LotteryIssue extends Model
{
protected $fillable = [
'issue_no',
'status',
'red1', 'red2', 'red3', 'blue',
'pool_amount',
'carry_amount',
'is_super_issue',
'no_winner_streak',
'total_tickets',
'payout_amount',
'sell_closes_at',
'draw_at',
];
/**
* 字段类型转换。
*/
protected function casts(): array
{
return [
'is_super_issue' => 'boolean',
'pool_amount' => 'integer',
'carry_amount' => 'integer',
'payout_amount' => 'integer',
'total_tickets' => 'integer',
'sell_closes_at' => 'datetime',
'draw_at' => 'datetime',
];
}
// ─── 关联 ──────────────────────────────────────────────────────────
/**
* 本期所有购票记录。
*/
public function tickets(): HasMany
{
return $this->hasMany(LotteryTicket::class, 'issue_id');
}
/**
* 本期奖池流水。
*/
public function poolLogs(): HasMany
{
return $this->hasMany(LotteryPoolLog::class, 'issue_id');
}
// ─── 静态查询 ──────────────────────────────────────────────────────
/**
* 获取当前正在购票的期次status=open
*/
public static function currentIssue(): ?static
{
return static::query()->where('status', 'open')->latest()->first();
}
/**
* 获取最新一期(不论状态)。
*/
public static function latestIssue(): ?static
{
return static::query()->latest()->first();
}
/**
* 生成下一期的期号(格式:年份 + 三位序号,如 2026001)。
*/
public static function nextIssueNo(): string
{
$year = now()->year;
$last = static::query()
->whereYear('created_at', $year)
->latest()
->first();
$seq = $last ? ((int) substr($last->issue_no, -3)) + 1 : 1;
return $year.str_pad($seq, 3, '0', STR_PAD_LEFT);
}
// ─── 业务方法 ──────────────────────────────────────────────────────
/**
* 判断当前期次是否仍在售票中。
*/
public function isOpen(): bool
{
return $this->status === 'open' && now()->lt($this->sell_closes_at);
}
/**
* 距离开奖剩余秒数。
*/
public function secondsUntilDraw(): int
{
if (! $this->draw_at) {
return 0;
}
return max(0, (int) now()->diffInSeconds($this->draw_at, false));
}
/**
* 判断给定号码是否与开奖号码匹配并返回奖级0=未中)。
*
* @param int $r1 用户红球1已排序
* @param int $r2 用户红球2
* @param int $r3 用户红球3
* @param int $b 用户蓝球
*/
public function calcPrizeLevel(int $r1, int $r2, int $r3, int $b): int
{
$userReds = [$r1, $r2, $r3];
$drawReds = [$this->red1, $this->red2, $this->red3];
$redMatches = count(array_intersect($userReds, $drawReds));
$blueMatch = ($b === $this->blue);
return match (true) {
$redMatches === 3 && $blueMatch => 1, // 一等奖
$redMatches === 3 && ! $blueMatch => 2, // 二等奖
$redMatches === 2 && $blueMatch => 3, // 三等奖
$redMatches === 2 && ! $blueMatch => 4, // 四等奖
$redMatches === 1 && $blueMatch => 5, // 五等奖
default => 0, // 未中奖
};
}
/**
* 返回奖级的中文标签。
*/
public static function prizeLevelLabel(int $level): string
{
return match ($level) {
1 => '一等奖',
2 => '二等奖',
3 => '三等奖',
4 => '四等奖',
5 => '五等奖',
default => '未中奖',
};
}
}