Compare commits

..

5 Commits

Author SHA1 Message Date
e5f83846b3 将项目中所有total_draw_count字段重构为total_ticket_count字段
将项目中所有paid_draw_count字段重构为paid_ticket_count字段
将项目中所有free_draw_count字段重构为free_ticket_count字段
2026-03-05 14:15:32 +08:00
5ab16243bd 重新设计状态码规范 2026-03-05 13:44:56 +08:00
8d8cee696f 重新登录或注册后清除掉原有用户的user-token保证只有一个用户能够登录 2026-03-05 12:25:59 +08:00
74612f136e 相同的设备标识dice只保证一个auth-token生效,清除掉多余的同一个dice多余的auth-token 2026-03-05 12:22:41 +08:00
13d8adbfe0 添加authToken和userToken 2026-03-05 12:17:20 +08:00
29 changed files with 583 additions and 362 deletions

View File

@@ -169,9 +169,9 @@
{ prop: 't3_wight', label: 'T3池权重', width: 100, formatter: weightFormatter('t3_wight') },
{ prop: 't4_wight', label: 'T4池权重', width: 100, formatter: weightFormatter('t4_wight') },
{ prop: 't5_wight', label: 'T5池权重', width: 100, formatter: weightFormatter('t5_wight') },
{ prop: 'total_draw_count', label: '总抽奖次数' },
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
{ prop: 'total_ticket_count', label: '总抽奖次数' },
{ prop: 'paid_ticket_count', label: '购买抽奖次数' },
{ prop: 'free_ticket_count', label: '赠送抽奖次数' },
{ prop: 'created_at', label: '创建时间' },
{ prop: 'updated_at', label: '更新时间' },
{ prop: 'operation', label: '操作', width: 120, fixed: 'right', useSlot: true }

View File

@@ -83,12 +83,12 @@
username: undefined,
use_coins_min: undefined,
use_coins_max: undefined,
total_draw_count_min: undefined,
total_draw_count_max: undefined,
paid_draw_count_min: undefined,
paid_draw_count_max: undefined,
free_draw_count_min: undefined,
free_draw_count_max: undefined,
total_ticket_count_min: undefined,
total_ticket_count_max: undefined,
paid_ticket_count_min: undefined,
paid_ticket_count_max: undefined,
free_ticket_count_min: undefined,
free_ticket_count_max: undefined,
create_time_min: undefined,
create_time_max: undefined,
create_time: undefined as [string, string] | undefined
@@ -131,9 +131,9 @@
{ prop: 'id', label: 'ID', width: 80 },
{ prop: 'player_id', label: '玩家用户名', formatter: (row: Record<string, any>) => usernameFormatter(row) },
{ prop: 'use_coins', label: '消耗硬币' },
{ prop: 'total_draw_count', label: '总抽奖次数' },
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
{ prop: 'total_ticket_count', label: '总抽奖次数' },
{ prop: 'paid_ticket_count', label: '购买抽奖次数' },
{ prop: 'free_ticket_count', label: '赠送抽奖次数' },
{ prop: 'remark', label: '备注', width: 100, showOverflowTooltip: true },
{ prop: 'create_time', label: '创建时间', width: 170 },
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }

View File

@@ -28,25 +28,25 @@
<el-form-item label="消耗硬币" prop="use_coins">
<el-input-number v-model="formData.use_coins" placeholder="请输入消耗硬币" :min="0" />
</el-form-item>
<el-form-item label="购买抽奖次数" prop="paid_draw_count">
<el-form-item label="购买抽奖次数" prop="paid_ticket_count">
<el-input-number
v-model="formData.paid_draw_count"
v-model="formData.paid_ticket_count"
placeholder="请输入购买抽奖次数"
:min="0"
@change="onDrawCountChange"
@change="onTicketCountChange"
/>
</el-form-item>
<el-form-item label="赠送抽奖次数" prop="free_draw_count">
<el-form-item label="赠送抽奖次数" prop="free_ticket_count">
<el-input-number
v-model="formData.free_draw_count"
v-model="formData.free_ticket_count"
placeholder="请输入赠送抽奖次数"
:min="0"
@change="onDrawCountChange"
@change="onTicketCountChange"
/>
</el-form-item>
<el-form-item label="总抽奖次数" prop="total_draw_count">
<el-form-item label="总抽奖次数" prop="total_ticket_count">
<el-input-number
:model-value="totalDrawCountComputed"
:model-value="totalTicketCountComputed"
placeholder="自动求和"
:min="0"
disabled
@@ -110,23 +110,23 @@
const rules = reactive<FormRules>({
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
use_coins: [{ required: true, message: '消耗硬币必需填写', trigger: 'blur' }],
paid_draw_count: [{ required: true, message: '购买抽奖次数必需填写', trigger: 'blur' }],
free_draw_count: [{ required: true, message: '赠送抽奖次数必需填写', trigger: 'blur' }],
paid_ticket_count: [{ required: true, message: '购买抽奖次数必需填写', trigger: 'blur' }],
free_ticket_count: [{ required: true, message: '赠送抽奖次数必需填写', trigger: 'blur' }],
remark: [{ required: true, message: '备注必需填写', trigger: 'blur' }]
})
/** 玩家下拉选项id、username */
const playerOptions = ref<Array<{ id: number; username: string }>>([])
/** 总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数(只读展示) */
const totalDrawCountComputed = computed(() => {
const paid = Number(formData.paid_draw_count) || 0
const free = Number(formData.free_draw_count) || 0
/** total_ticket_count = paid_ticket_count + free_ticket_count(只读展示) */
const totalTicketCountComputed = computed(() => {
const paid = Number(formData.paid_ticket_count) || 0
const free = Number(formData.free_ticket_count) || 0
return paid + free
})
function onDrawCountChange() {
formData.total_draw_count = totalDrawCountComputed.value
function onTicketCountChange() {
formData.total_ticket_count = totalTicketCountComputed.value
}
/**
@@ -136,9 +136,9 @@
id: null,
player_id: null,
use_coins: null as number | null,
total_draw_count: null as number | null,
paid_draw_count: null as number | null,
free_draw_count: null as number | null,
total_ticket_count: null as number | null,
paid_ticket_count: null as number | null,
free_ticket_count: null as number | null,
remark: ''
}
@@ -188,9 +188,9 @@
'id',
'player_id',
'use_coins',
'total_draw_count',
'paid_draw_count',
'free_draw_count',
'total_ticket_count',
'paid_ticket_count',
'free_ticket_count',
'remark'
]
keys.forEach((key) => {
@@ -210,12 +210,12 @@
}
/**
* 提交表单(总抽奖次数由购买+赠送自动求和,提交前写入)
* 提交表单(total_ticket_count 由 paid_ticket_count + free_ticket_count 自动求和,提交前写入)
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
formData.total_draw_count = totalDrawCountComputed.value
formData.total_ticket_count = totalTicketCountComputed.value
await formRef.value.validate()
if (props.dialogType === 'add') {
const rest = { ...formData } as Record<string, unknown>

View File

@@ -35,10 +35,10 @@
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="总抽奖次数" prop="total_draw_count_min">
<el-form-item label="总抽奖次数" prop="total_ticket_count_min">
<div class="range-wrap">
<el-input-number
v-model="formData.total_draw_count_min"
v-model="formData.total_ticket_count_min"
placeholder="最小"
:min="0"
controls-position="right"
@@ -46,7 +46,7 @@
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.total_draw_count_max"
v-model="formData.total_ticket_count_max"
placeholder="最大"
:min="0"
controls-position="right"
@@ -56,10 +56,10 @@
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="购买抽奖次数" prop="paid_draw_count_min">
<el-form-item label="购买抽奖次数" prop="paid_ticket_count_min">
<div class="range-wrap">
<el-input-number
v-model="formData.paid_draw_count_min"
v-model="formData.paid_ticket_count_min"
placeholder="最小"
:min="0"
controls-position="right"
@@ -67,7 +67,7 @@
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.paid_draw_count_max"
v-model="formData.paid_ticket_count_max"
placeholder="最大"
:min="0"
controls-position="right"
@@ -77,10 +77,10 @@
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="赠送抽奖次数" prop="free_draw_count_min">
<el-form-item label="赠送抽奖次数" prop="free_ticket_count_min">
<div class="range-wrap">
<el-input-number
v-model="formData.free_draw_count_min"
v-model="formData.free_ticket_count_min"
placeholder="最小"
:min="0"
controls-position="right"
@@ -88,7 +88,7 @@
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.free_draw_count_max"
v-model="formData.free_ticket_count_max"
placeholder="最大"
:min="0"
controls-position="right"

View File

@@ -196,9 +196,9 @@
align: 'center',
showOverflowTooltip: true
},
{ prop: 'total_draw_count', label: '总抽奖次数', align: 'center' },
{ prop: 'paid_draw_count', label: '购买抽奖次数', align: 'center' },
{ prop: 'free_draw_count', label: '赠送抽奖次数', align: 'center' },
{ prop: 'total_ticket_count', label: '总抽奖次数', align: 'center' },
{ prop: 'paid_ticket_count', label: '购买抽奖次数', align: 'center' },
{ prop: 'free_ticket_count', label: '赠送抽奖次数', align: 'center' },
{ prop: 'create_time', label: '创建时间', width: 170, align: 'center' },
{
prop: 'operation',

View File

@@ -17,10 +17,14 @@ REDIS_PASSWORD = ''
REDIS_DB = 0
# API 鉴权与用户(可选,不填则用默认值)
# API_AUTH_TOKEN_EXP = 86400
# authToken 签名密钥(必填,与客户端约定,用于 signature 校验)
API_AUTH_TOKEN_SECRET = xF75oK91TQj13s0UmNIr1NBWMWGfflNO
# authToken 时间戳允许误差秒数,防重放,默认 300
API_AUTH_TOKEN_TIME_TOLERANCE = 300
API_AUTH_TOKEN_EXP = 86400
# API_USER_TOKEN_EXP = 604800
# API_USER_CACHE_EXPIRE = 604800
# API_USER_ENCRYPT_KEY = dafuweng_api_user_cache_key_32
API_USER_CACHE_EXPIRE = 86400
API_USER_ENCRYPT_KEY = Wj818SK8dhKBKNOY3PUTmZfhQDMCXEZi
# 验证码配置,支持cache|session
CAPTCHA_MODE = cache

54
server/app/api/cache/AuthTokenCache.php vendored Normal file
View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace app\api\cache;
use support\think\Cache;
/**
* 按设备标识存储当前有效的 auth-token同一设备只保留最新一个旧 token 自动失效
*/
class AuthTokenCache
{
private static function prefix(): string
{
return config('api.auth_token_device_prefix', 'api:auth_token:');
}
/**
* 设置该设备当前有效的 auth-token会覆盖同设备之前的 token使旧 token 失效)
* @param string $device 设备标识,如 dice
* @param string $token 完整 auth-token 字符串
* @param int $ttl 过期时间(秒),应与 auth_token_exp 一致
*/
public static function setDeviceToken(string $device, string $token, int $ttl): bool
{
if ($device === '' || $ttl <= 0) {
return false;
}
$key = self::prefix() . $device;
return Cache::set($key, $token, $ttl);
}
/**
* 获取该设备当前有效的 auth-token不存在或已过期返回 null
*/
public static function getDeviceToken(string $device): ?string
{
if ($device === '') {
return null;
}
$key = self::prefix() . $device;
$value = Cache::get($key);
return $value !== null && $value !== '' ? (string) $value : null;
}
/**
* 校验请求中的 token 是否为该设备当前唯一有效 token
*/
public static function isCurrentToken(string $device, string $token): bool
{
$current = self::getDeviceToken($device);
return $current !== null && $current === $token;
}
}

View File

@@ -131,4 +131,51 @@ class UserCache
$val = Cache::get($key);
return $val !== null && $val !== '';
}
/** 当前有效 user-token 按用户存储的 key 前缀(重新登录/注册后覆盖,保证单用户单 token */
private static function currentTokenPrefix(): string
{
return config('api.user_token_current_prefix', 'api:user:current_token:');
}
private static function userTokenExpire(): int
{
return (int) config('api.user_token_exp', 604800);
}
/**
* 设置该用户当前唯一有效的 user-token登录/注册时调用,会覆盖该用户之前的 token
* @param int $userId 用户 ID
* @param string $token 完整 user-token 字符串
*/
public static function setCurrentUserToken(int $userId, string $token): bool
{
if ($userId <= 0 || $token === '') {
return false;
}
$key = self::currentTokenPrefix() . $userId;
return Cache::set($key, $token, self::userTokenExpire());
}
/**
* 获取该用户当前在服务端登记的有效 user-token不存在或已过期返回 null
*/
public static function getCurrentUserToken(int $userId): ?string
{
if ($userId <= 0) {
return null;
}
$key = self::currentTokenPrefix() . $userId;
$value = Cache::get($key);
return $value !== null && $value !== '' ? (string) $value : null;
}
/**
* 校验请求中的 token 是否为该用户当前唯一有效 token
*/
public static function isCurrentUserToken(int $userId, string $token): bool
{
$current = self::getCurrentUserToken($userId);
return $current !== null && $current === $token;
}
}

View File

@@ -7,29 +7,84 @@ use support\Request;
use support\Response;
use Tinywan\Jwt\JwtToken;
use plugin\saiadmin\basic\OpenController;
use app\api\util\ReturnCode;
use app\api\cache\AuthTokenCache;
/**
* API 鉴权 Token 接口
* 仅支持 GET必传参数signature、secret、device、time签名规则signature = md5(device . secret . time)
* 后续所有 /api 接口调用均需在请求头携带此接口返回的 auth-token
*/
class AuthTokenController extends OpenController
{
/**
* 获取 auth-token
* GET 或 POST /api/authToken
* GET /api/authToken
* 参数signature签名、secret密钥、device设备标识、time时间戳四者均为必传且非空
*/
public function index(Request $request): Response
{
$exp = config('api.auth_token_exp', 86400);
if (strtoupper($request->method()) !== 'GET') {
return $this->fail('仅支持 GET 请求', ReturnCode::PARAMS_ERROR);
}
$param = $request->get();
$signature = trim((string) ($param['signature'] ?? ''));
$secret = trim((string) ($param['secret'] ?? ''));
$device = trim((string) ($param['device'] ?? ''));
$time = trim((string) ($param['time'] ?? ''));
if ($signature === '' || $secret === '' || $device === '' || $time === '') {
return $this->fail('signature、secret、device、time 均为必传且不能为空', ReturnCode::PARAMS_ERROR);
}
$serverSecret = trim((string) config('api.auth_token_secret', ''));
if ($serverSecret === '') {
return $this->fail('服务未配置 API_AUTH_TOKEN_SECRET', ReturnCode::PARAMS_ERROR);
}
if ($secret !== $serverSecret) {
return $this->fail('密钥错误', ReturnCode::FORBIDDEN);
}
$tolerance = (int) config('api.auth_token_time_tolerance', 300);
$now = time();
$ts = is_numeric($time) ? (int) $time : 0;
if ($ts <= 0 || abs($now - $ts) > $tolerance) {
return $this->fail('时间戳无效或已过期', ReturnCode::PARAMS_ERROR);
}
$sign = $this->getAuthToken($device, $serverSecret, $time);
if ($sign !== $signature) {
return $this->fail('签名验证失败', ReturnCode::FORBIDDEN);
}
$exp = (int) config('api.auth_token_exp', 86400);
$tokenResult = JwtToken::generateToken([
'id' => 0,
'plat' => 'api',
'device' => $device,
'access_exp' => $exp,
]);
// 同一设备只保留最新 token覆盖后旧 token 失效
AuthTokenCache::setDeviceToken($device, $tokenResult['access_token'], $exp);
return $this->success([
'auth-token' => $tokenResult['access_token'],
'expires_in' => $tokenResult['expires_in'],
]);
}
/**
* 生成签名signature = md5(device . secret . time)
*
* @param string $device 设备标识
* @param string $secret 密钥(来自配置)
* @param string $time 时间戳
* @return string
*/
private function getAuthToken(string $device, string $secret, string $time): string
{
return md5($device . $secret . $time);
}
}

View File

@@ -5,9 +5,9 @@ namespace app\api\controller;
use support\Request;
use support\Response;
use app\api\logic\UserLogic;
use app\api\logic\GameLogic;
use app\api\logic\PlayStartLogic;
use app\api\logic\UserLogic;
use app\api\util\ReturnCode;
use app\dice\model\play_record\DicePlayRecord;
use app\dice\model\player\DicePlayer;
@@ -23,30 +23,15 @@ class GameController extends OpenController
/**
* 购买抽奖券
* POST /api/game/buyLotteryTickets
* header: auth-token, user-token
* header: auth-token, user-token(由 CheckUserTokenMiddleware 注入 request->user_id
* body: count = 1 | 5 | 101次/100coin, 5次/500coin, 10次/1000coin
* 记录钱包流水,并更新缓存中玩家的 total_draw_count、paid_draw_count、free_draw_count、coin
*/
public function buyLotteryTickets(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT);
}
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
$count = (int) $request->post('count', 0);
if (!in_array($count, [1, 5, 10], true)) {
return $this->fail('购买抽奖券错误', ReturnCode::EMPTY_PARAMS);
return $this->fail('购买抽奖券错误', ReturnCode::PARAMS_ERROR);
}
try {
@@ -60,7 +45,7 @@ class GameController extends OpenController
$coin = $player ? (float) $player->coin : 0;
return $this->success(['coin' => $coin], $msg);
}
return $this->fail($msg, ReturnCode::EMPTY_PARAMS);
return $this->fail($msg, ReturnCode::BUSINESS_ERROR);
}
}
@@ -79,39 +64,24 @@ class GameController extends OpenController
/**
* 开始游戏(抽奖一局)
* POST /api/game/playStart
* header: auth-token, user-token
* header: auth-token, user-token(由 CheckUserTokenMiddleware 注入 request->user_id
* body: rediction 必传0=无 1=中奖
* 余额不足时返回 code=200、message=玩家当前余额不足无法开启对局;超时返回 code=200、message=服务超时,并记录 status=0
*/
public function playStart(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT);
}
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
$rediction = $request->post('rediction');
if ($rediction === '' || $rediction === null) {
return $this->fail('请传递 rediction 参数', ReturnCode::EMPTY_PARAMS);
return $this->fail('请传递 rediction 参数', ReturnCode::PARAMS_ERROR);
}
$direction = (int) $rediction;
if (!in_array($direction, [0, 1], true)) {
return $this->fail('rediction 必须为 0 或 1', ReturnCode::EMPTY_PARAMS);
return $this->fail('rediction 必须为 0 或 1', ReturnCode::PARAMS_ERROR);
}
$player = DicePlayer::find($userId);
if (!$player) {
return $this->fail('用户不存在', ReturnCode::EMPTY_PARAMS);
return $this->fail('用户不存在', ReturnCode::NOT_FOUND);
}
$minEv = (float) DiceRewardConfig::min('real_ev');
$minCoin = abs($minEv + 100);
@@ -125,7 +95,7 @@ class GameController extends OpenController
$data = $logic->run($userId, $direction);
return $this->success($data);
} catch (ApiException $e) {
return $this->fail($e->getMessage(), ReturnCode::EMPTY_PARAMS);
return $this->fail($e->getMessage(), ReturnCode::BUSINESS_ERROR);
} catch (\Throwable $e) {
$timeoutRecord = null;
try {

View File

@@ -28,7 +28,7 @@ class UserController extends OpenController
$phone = $request->post('phone', '');
$password = $request->post('password', '');
if ($phone === '' || $password === '') {
return $this->fail('请填写手机号和密码', ReturnCode::EMPTY_PARAMS);
return $this->fail('请填写手机号和密码', ReturnCode::PARAMS_ERROR);
}
$logic = new UserLogic();
$data = $logic->login($phone, $password);
@@ -50,7 +50,7 @@ class UserController extends OpenController
$password = $request->post('password', '');
$nickname = $request->post('nickname');
if ($phone === '' || $password === '') {
return $this->fail('请填写手机号和密码', ReturnCode::EMPTY_PARAMS);
return $this->fail('请填写手机号和密码', ReturnCode::PARAMS_ERROR);
}
$logic = new UserLogic();
$data = $logic->register($phone, $password, $nickname ? (string) $nickname : null);
@@ -64,54 +64,31 @@ class UserController extends OpenController
/**
* 退出登录
* POST /api/user/logout
* header: user-token或 Authorization: Bearer <user-token>
* 将当前 user-token 加入黑名单,之后该 token 无法再用于获取 user_id
* header: user-token由 CheckUserTokenMiddleware 校验并注入 request->userToken
*/
public function logout(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
$token = $request->userToken ?? UserLogic::getTokenFromRequest($request);
if ($token === '' || !UserLogic::logout($token)) {
return $this->fail('退出失败或 token 已失效', ReturnCode::TOKEN_INVALID);
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
if (UserLogic::logout($token)) {
return $this->success('已退出登录');
}
return $this->fail('退出失败或 token 已失效', ReturnCode::TOKEN_TIMEOUT);
return $this->success('已退出登录');
}
/**
* 获取当前用户信息
* GET /api/user/info
* header: user-token或 Authorization: Bearer <user-token>
* 返回id, username, phone, uid, name, coin, total_draw_count
* header: user-token由 CheckUserTokenMiddleware 校验并注入 request->user_id
* 返回id, username, phone, uid, name, coin, total_ticket_count
*/
public function info(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT);
}
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
$user = UserLogic::getCachedUser($userId);
if (empty($user)) {
return $this->fail('用户不存在', ReturnCode::EMPTY_PARAMS);
return $this->fail('用户不存在', ReturnCode::NOT_FOUND);
}
$fields = ['id', 'username', 'phone', 'uid', 'name', 'coin', 'total_draw_count'];
$fields = ['id', 'username', 'phone', 'uid', 'name', 'coin', 'total_ticket_count'];
$info = [];
foreach ($fields as $field) {
if (array_key_exists($field, $user)) {
@@ -122,30 +99,16 @@ class UserController extends OpenController
}
/**
* 获取钱包余额(读缓存,不查库,低延迟
* 获取钱包余额(优先读缓存,缓存未命中时从库拉取并回写缓存
* GET /api/user/balance
* header: user-token或 Authorization: Bearer <user-token>
* 返回coin, phone, username登录时已写入缓存本接口只从缓存读取
* header: user-token由 CheckUserTokenMiddleware 校验并注入 request->user_id
*/
public function balance(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT);
}
$user = UserCache::getUser($userId);
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
$user = UserLogic::getCachedUser($userId);
if (empty($user)) {
return $this->fail('缓存已过期,请重新登录', ReturnCode::TOKEN_TIMEOUT);
return $this->fail('用户不存在', ReturnCode::NOT_FOUND);
}
$coin = $user['coin'] ?? 0;
if (is_string($coin) && is_numeric($coin)) {
@@ -161,26 +124,12 @@ class UserController extends OpenController
/**
* 玩家钱包流水
* GET /api/user/walletRecord
* header: user-token或 Authorization: Bearer <user-token>
* header: user-token由 CheckUserTokenMiddleware 校验并注入 request->user_id
* 参数: page 页码默认1, limit 每页条数默认10, create_time_min/create_time_max 创建时间范围(可选)
*/
public function walletRecord(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT);
}
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
$page = (int) $request->post('page', 1);
$limit = (int) $request->post('limit', 10);
if ($page < 1) {
@@ -217,26 +166,12 @@ class UserController extends OpenController
/**
* 游玩记录
* GET /api/user/playGameRecord
* header: user-token或 Authorization: Bearer <user-token>
* header: user-token由 CheckUserTokenMiddleware 校验并注入 request->user_id
* 参数: page 页码默认1, limit 每页条数默认10, create_time_min/create_time_max 创建时间范围(可选)
*/
public function playGameRecord(Request $request): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT);
}
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
$page = (int) $request->post('page', 1);
$limit = (int) $request->post('limit', 10);
if ($page < 1) {

View File

@@ -29,7 +29,7 @@ class GameLogic
* 购买抽奖券
* @param int $playerId 玩家ID即 user_id
* @param int $count 购买档位1 / 5 / 10
* @return array 更新后的 coin, total_draw_count, paid_draw_count, free_draw_count
* @return array 更新后的 coin, total_ticket_count, paid_ticket_count, free_ticket_count
*/
public function buyLotteryTickets(int $playerId, int $count): array
{
@@ -52,9 +52,9 @@ class GameLogic
}
$coinAfter = $coinBefore - $cost;
$totalBefore = (int) ($player->total_draw_count ?? 0);
$paidBefore = (int) ($player->paid_draw_count ?? 0);
$freeBefore = (int) ($player->free_draw_count ?? 0);
$totalBefore = (int) ($player->total_ticket_count ?? 0);
$paidBefore = (int) ($player->paid_ticket_count ?? 0);
$freeBefore = (int) ($player->free_ticket_count ?? 0);
Db::transaction(function () use (
$player,
@@ -70,9 +70,9 @@ class GameLogic
$freeBefore
) {
$player->coin = $coinAfter;
$player->total_draw_count = $totalBefore + $addTotal;
$player->paid_draw_count = $paidBefore + $addPaid;
$player->free_draw_count = $freeBefore + $addFree;
$player->total_ticket_count = $totalBefore + $addTotal;
$player->paid_ticket_count = $paidBefore + $addPaid;
$player->free_ticket_count = $freeBefore + $addFree;
$player->save();
// 钱包流水记录
@@ -82,9 +82,9 @@ class GameLogic
'type' => self::WALLET_TYPE_BUY_DRAW,
'wallet_before' => $coinBefore,
'wallet_after' => $coinAfter,
'total_draw_count' => $addTotal,
'paid_draw_count' => $addPaid,
'free_draw_count' => $addFree,
'total_ticket_count' => $addTotal,
'paid_ticket_count' => $addPaid,
'free_ticket_count' => $addFree,
'remark' => "购买抽奖券{$addTotal}次(付费{$addPaid}次+赠送{$addFree}次)",
]);
@@ -92,9 +92,9 @@ class GameLogic
DicePlayerTicketRecord::create([
'player_id' => $playerId,
'use_coins' => $cost,
'total_draw_count' => $addTotal,
'paid_draw_count' => $addPaid,
'free_draw_count' => $addFree,
'total_ticket_count' => $addTotal,
'paid_ticket_count' => $addPaid,
'free_ticket_count' => $addFree,
'remark' => "购买抽奖券{$addTotal}次(付费{$addPaid}次+赠送{$addFree}次)",
]);
});
@@ -105,9 +105,9 @@ class GameLogic
return [
'coin' => (float) $updated->coin,
'total_draw_count' => (int) $updated->total_draw_count,
'paid_draw_count' => (int) $updated->paid_draw_count,
'free_draw_count' => (int) $updated->free_draw_count,
'total_ticket_count' => (int) $updated->total_ticket_count,
'paid_ticket_count' => (int) $updated->paid_ticket_count,
'free_ticket_count' => (int) $updated->free_ticket_count,
];
}
}

View File

@@ -54,8 +54,8 @@ class PlayStartLogic
throw new ApiException('当前玩家余额小于DiceRewardConfigMin.real_ev+100无法继续游戏');
}
$paid = (int) ($player->paid_draw_count ?? 0);
$free = (int) ($player->free_draw_count ?? 0);
$paid = (int) ($player->paid_ticket_count ?? 0);
$free = (int) ($player->free_ticket_count ?? 0);
if ($paid + $free <= 0) {
throw new ApiException('抽奖券不足');
}
@@ -125,21 +125,21 @@ class PlayStartLogic
$coinBefore = (float) $p->coin;
$coinAfter = $coinBefore + $winCoin;
$p->coin = $coinAfter;
$p->total_draw_count = max(0, (int) $p->total_draw_count - 1);
$p->total_ticket_count = max(0, (int) $p->total_ticket_count - 1);
if ($ticketType === self::LOTTERY_TYPE_PAID) {
$p->paid_draw_count = max(0, (int) $p->paid_draw_count - 1);
$p->paid_ticket_count = max(0, (int) $p->paid_ticket_count - 1);
} else {
$p->free_draw_count = max(0, (int) $p->free_draw_count - 1);
$p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1);
}
// 若本局中奖档位为 T5则额外赠送 1 次免费抽奖次数(总次数也 +1并记录抽奖券获取记录
if ($isTierT5) {
$p->free_draw_count = (int) $p->free_draw_count + 1;
$p->total_draw_count = (int) $p->total_draw_count + 1;
$p->free_ticket_count = (int) $p->free_ticket_count + 1;
$p->total_ticket_count = (int) $p->total_ticket_count + 1;
DicePlayerTicketRecord::create([
'player_id' => $playerId,
'free_draw_count' => 1,
'free_ticket_count' => 1,
'remark' => '中奖结果为T5',
]);
}

View File

@@ -56,6 +56,8 @@ class UserLogic
UserCache::setUser((int) $user->id, $userArr);
$userToken = $this->generateUserToken((int) $user->id);
// 同一用户只保留最新一次登录的 token旧 token 自动失效
UserCache::setCurrentUserToken((int) $user->id, $userToken);
return [
'user' => $userArr,
'user-token' => $userToken,
@@ -94,6 +96,8 @@ class UserLogic
UserCache::setUser((int) $user->id, $userArr);
$userToken = $this->generateUserToken((int) $user->id);
// 同一用户只保留最新一次登录的 token旧 token 自动失效
UserCache::setCurrentUserToken((int) $user->id, $userToken);
return [
'user' => $userArr,
'user-token' => $userToken,
@@ -123,6 +127,41 @@ class UserLogic
return $result['access_token'];
}
/**
* 从请求中解析 user-tokenheader: user-token 或 Authorization: Bearer
* @param object $request 需有 header(string $name) 方法
*/
public static function getTokenFromRequest(object $request): string
{
$token = $request->header('user-token') ?? '';
if ($token !== '') {
return trim((string) $token);
}
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
return trim(substr($auth, 7));
}
return '';
}
/**
* 从请求获取当前用户 ID优先 request->user_id否则从 header 的 user-token 解析
* 中间件未正确注入时仍可兜底解析
* @param object $request 需有 user_id 属性及 header() 方法
*/
public static function getUserIdFromRequest(object $request): ?int
{
$id = $request->user_id ?? null;
if ($id !== null && (int) $id > 0) {
return (int) $id;
}
$token = self::getTokenFromRequest($request);
if ($token === '') {
return null;
}
return self::getUserIdFromToken($token);
}
/**
* 根据 user-token 获取 user_id不写缓存仅解析 JWT
* 若 token 已通过退出接口加入黑名单,返回 null
@@ -139,7 +178,15 @@ class UserLogic
return null;
}
$id = $extend['id'] ?? null;
return $id !== null ? (int) $id : null;
if ($id === null) {
return null;
}
$userId = (int) $id;
// 同一用户只允许当前登记的 token 生效,重新登录/注册后旧 token 失效
if (!UserCache::isCurrentUserToken($userId, $userToken)) {
return null;
}
return $userId;
} catch (\Throwable $e) {
return null;
}

View File

@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace app\api\middleware;
use support\Log;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use Tinywan\Jwt\JwtToken;
use Tinywan\Jwt\Exception\JwtTokenException;
use Tinywan\Jwt\Exception\JwtTokenExpiredException;
use app\api\util\ReturnCode;
use plugin\saiadmin\exception\ApiException;
/**
* API 鉴权中间件
* 校验请求头 auth-token或 Authorization: Bearer xxx白名单路径不校验
*/
class CheckApiAuthMiddleware implements MiddlewareInterface
{
/** 不需要 auth-token 的路径(仅获取 token 的接口) */
private const WHITELIST = [
'api/authToken',
];
public function process(Request $request, callable $handler): Response
{
$path = trim($request->path(), '/');
if ($this->isWhitelist($path)) {
return $handler($request);
}
$token = $request->header('auth-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
throw new ApiException('请携带 auth-token', ReturnCode::MISSING_TOKEN);
}
try {
// ACCESS_TOKEN = 1JwtToken 内部私有常量)
$decoded = JwtToken::verify(1, $token);
$extend = $decoded['extend'] ?? [];
if (($extend['plat'] ?? '') !== 'api') {
throw new ApiException('auth-token 无效', ReturnCode::TOKEN_TIMEOUT);
}
} catch (JwtTokenExpiredException $e) {
Log::error('auth-token 已过期, 报错信息'. $e);
throw new ApiException('auth-token 已过期', ReturnCode::TOKEN_TIMEOUT);
} catch (JwtTokenException $e) {
Log::error('auth-token 无效, 报错信息'. $e);
throw new ApiException($e->getMessage() ?: 'auth-token 无效', ReturnCode::TOKEN_TIMEOUT);
} catch (\Throwable $e) {
Log::error('auth-token 校验失败, 报错信息'. $e);
throw new ApiException('auth-token 校验失败', ReturnCode::TOKEN_TIMEOUT);
}
return $handler($request);
}
private function isWhitelist(string $path): bool
{
foreach (self::WHITELIST as $prefix) {
if ($path === $prefix || str_starts_with($path, $prefix . '/')) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace app\api\middleware;
use support\Log;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use Tinywan\Jwt\JwtToken;
use Tinywan\Jwt\Exception\JwtTokenException;
use Tinywan\Jwt\Exception\JwtTokenExpiredException;
use app\api\util\ReturnCode;
use app\api\cache\AuthTokenCache;
use plugin\saiadmin\exception\ApiException;
/**
* 仅校验 auth-token 请求头
* 白名单路径(如 /api/authToken不校验其它接口必须携带有效 auth-token 或 Authorization: Bearer <token>,且必须通过 JWT 签名与 plat=api 校验
*/
class CheckAuthTokenMiddleware implements MiddlewareInterface
{
/** 不需要 auth-token 的路径 */
private const WHITELIST = [
'api/authToken',
];
/** JWT 至少为 xxx.yyy.zzz 三段 */
private const JWT_PARTS_MIN = 3;
public function process(Request $request, callable $handler): Response
{
$path = trim((string) $request->path(), '/');
if ($this->isWhitelist($path)) {
return $handler($request);
}
$token = $this->getAuthTokenFromRequest($request);
if ($token === '') {
throw new ApiException('请携带 auth-token', ReturnCode::UNAUTHORIZED);
}
if (!$this->looksLikeJwt($token)) {
throw new ApiException('auth-token 格式无效', ReturnCode::TOKEN_INVALID);
}
$decoded = $this->verifyAuthToken($token);
$extend = $decoded['extend'] ?? [];
if (($extend['plat'] ?? '') !== 'api') {
throw new ApiException('auth-token 无效(非 API 凭证)', ReturnCode::TOKEN_INVALID);
}
// 同一设备只允许一个 auth-token 生效,非当前 token 视为已失效
$device = (string) ($extend['device'] ?? '');
if ($device !== '' && !AuthTokenCache::isCurrentToken($device, $token)) {
throw new ApiException('auth-token 已失效(该设备已签发新凭证,请使用新 auth-token', ReturnCode::TOKEN_INVALID);
}
return $handler($request);
}
private function getAuthTokenFromRequest(Request $request): string
{
$token = $request->header('auth-token');
if ($token !== null && $token !== '') {
return trim((string) $token);
}
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
return trim(substr($auth, 7));
}
return '';
}
private function looksLikeJwt(string $token): bool
{
$parts = explode('.', $token);
return count($parts) >= self::JWT_PARTS_MIN;
}
/**
* 校验 auth-token 有效性签名、过期、iss 等),无效或过期必抛 ApiException
*/
private function verifyAuthToken(string $token): array
{
try {
return JwtToken::verify(1, $token);
} catch (JwtTokenExpiredException $e) {
Log::error('auth-token 已过期, 报错信息' . $e);
throw new ApiException('auth-token 已过期', ReturnCode::TOKEN_INVALID);
} catch (JwtTokenException $e) {
Log::error('auth-token 无效, 报错信息' . $e);
throw new ApiException($e->getMessage() ?: 'auth-token 无效', ReturnCode::TOKEN_INVALID);
} catch (\Throwable $e) {
Log::error('auth-token 校验失败, 报错信息' . $e);
throw new ApiException('auth-token 校验失败', ReturnCode::TOKEN_INVALID);
}
}
private function isWhitelist(string $path): bool
{
foreach (self::WHITELIST as $prefix) {
if ($path === $prefix || str_starts_with($path, $prefix . '/')) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace app\api\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use app\api\logic\UserLogic;
use app\api\util\ReturnCode;
use plugin\saiadmin\exception\ApiException;
/**
* 校验 user-token 请求头
* 从 header 读取 user-token 或 Authorization: Bearer <user-token>,校验通过后将 user_id、userToken 写入 request 供控制器使用
*/
class CheckUserTokenMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
$token = $request->header('user-token');
if (empty($token)) {
$auth = $request->header('authorization');
if ($auth && stripos($auth, 'Bearer ') === 0) {
$token = trim(substr($auth, 7));
}
}
if (empty($token)) {
throw new ApiException('请携带 user-token', ReturnCode::UNAUTHORIZED);
}
$userId = UserLogic::getUserIdFromToken($token);
if ($userId === null) {
throw new ApiException('user-token 无效或已过期', ReturnCode::TOKEN_INVALID);
}
$request->user_id = $userId;
$request->userToken = $token;
return $handler($request);
}
}

View File

@@ -109,7 +109,7 @@ class LotteryService
return $tiers[4];
}
/** 按 paid_draw_count 与 free_draw_count 权重随机抽取 0=付费 1=免费 */
/** 按 paid_ticket_count 与 free_ticket_count 权重随机抽取 0=付费 1=免费 */
public static function drawTicketType(int $paid, int $free): int
{
if ($paid <= 0 && $free <= 0) {

View File

@@ -4,19 +4,32 @@ declare(strict_types=1);
namespace app\api\util;
/**
* API 状态码统一管理
* API 统一状态码
* 与 HTTP 语义对齐,便于前端与网关处理
*/
class ReturnCode
{
/** 200 成功 */
public const SUCCESS = 200;
/** 201 请携带 tokenauth-token / user-token */
public const MISSING_TOKEN = 201;
/** 400 请求参数错误(缺少参数、参数无效、格式错误等 */
public const PARAMS_ERROR = 400;
/** 202 缺少参数 / 参数错误 / 业务校验不通过(如余额不足、购买抽奖券错误等 */
public const EMPTY_PARAMS = 202;
/** 401 未授权(未携带 auth-token 或 user-token */
public const UNAUTHORIZED = 401;
/** 203 token 过期或无效auth-token / user-token 过期、缓存已过期等) */
public const TOKEN_TIMEOUT = 203;
/** 402 token 无效或已过期(格式无效、签名错误、过期、非当前有效 token 等) */
public const TOKEN_INVALID = 402;
/** 403 鉴权失败(密钥错误、签名验证失败等) */
public const FORBIDDEN = 403;
/** 404 资源不存在(用户不存在等) */
public const NOT_FOUND = 404;
/** 422 业务逻辑错误(余额不足、购买失败、业务校验不通过等) */
public const BUSINESS_ERROR = 422;
/** 500 服务器内部错误 */
public const SERVER_ERROR = 500;
}

View File

@@ -41,12 +41,12 @@ class DicePlayerTicketRecordController extends BaseController
['username', ''],
['use_coins_min', ''],
['use_coins_max', ''],
['total_draw_count_min', ''],
['total_draw_count_max', ''],
['paid_draw_count_min', ''],
['paid_draw_count_max', ''],
['free_draw_count_min', ''],
['free_draw_count_max', ''],
['total_ticket_count_min', ''],
['total_ticket_count_max', ''],
['paid_ticket_count_min', ''],
['paid_ticket_count_max', ''],
['free_ticket_count_min', ''],
['free_ticket_count_max', ''],
['create_time_min', ''],
['create_time_max', ''],
]);

View File

@@ -25,28 +25,28 @@ class DicePlayerTicketRecordLogic extends BaseLogic
}
/**
* 添加前:总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数
* 添加前:total_ticket_count = paid_ticket_count + free_ticket_count
*/
public function add(array $data): mixed
{
$data = $this->fillTotalDrawCount($data);
$data = $this->fillTotalTicketCount($data);
return parent::add($data);
}
/**
* 修改前:总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数
* 修改前:total_ticket_count = paid_ticket_count + free_ticket_count
*/
public function edit($id, array $data): mixed
{
$data = $this->fillTotalDrawCount($data);
$data = $this->fillTotalTicketCount($data);
return parent::edit($id, $data);
}
private function fillTotalDrawCount(array $data): array
private function fillTotalTicketCount(array $data): array
{
$paid = isset($data['paid_draw_count']) ? (int) $data['paid_draw_count'] : 0;
$free = isset($data['free_draw_count']) ? (int) $data['free_draw_count'] : 0;
$data['total_draw_count'] = $paid + $free;
$paid = isset($data['paid_ticket_count']) ? (int) $data['paid_ticket_count'] : 0;
$free = isset($data['free_ticket_count']) ? (int) $data['free_ticket_count'] : 0;
$data['total_ticket_count'] = $paid + $free;
return $data;
}
}

View File

@@ -29,9 +29,9 @@ class DicePlayerWalletRecordLogic extends BaseLogic
*/
public function add(array $data): mixed
{
$data['total_draw_count'] = $data['total_draw_count'] ?? 0;
$data['paid_draw_count'] = $data['paid_draw_count'] ?? 0;
$data['free_draw_count'] = $data['free_draw_count'] ?? 0;
$data['total_ticket_count'] = $data['total_ticket_count'] ?? 0;
$data['paid_ticket_count'] = $data['paid_ticket_count'] ?? 0;
$data['free_ticket_count'] = $data['free_ticket_count'] ?? 0;
return parent::add($data);
}
@@ -81,9 +81,9 @@ class DicePlayerWalletRecordLogic extends BaseLogic
'wallet_after' => $walletAfter,
'remark' => $remark,
'user_id' => $adminId,
'total_draw_count' => 0,
'paid_draw_count' => 0,
'free_draw_count' => 0,
'total_ticket_count' => 0,
'paid_ticket_count' => 0,
'free_ticket_count' => 0,
];
return $this->model->create($record);

View File

@@ -28,9 +28,9 @@ use app\dice\model\lottery_config\DiceLotteryConfig;
* @property $t3_wight T3池权重
* @property $t4_wight T4池权重
* @property $t5_wight T5池权重
* @property $total_draw_count 总抽奖次数
* @property $paid_draw_count 购买抽奖次数
* @property $free_draw_count 赠送抽奖次数
* @property $total_ticket_count 总抽奖次数
* @property $paid_ticket_count 购买抽奖次数
* @property $free_ticket_count 赠送抽奖次数
* @property $created_at 创建时间
* @property $updated_at 更新时间
* @property $deleted_at 删除时间

View File

@@ -18,9 +18,9 @@ use think\model\relation\BelongsTo;
* @property $id ID
* @property $player_id 玩家id
* @property $use_coins 消耗硬币
* @property $total_draw_count 总抽奖次数
* @property $paid_draw_count 购买抽奖次数
* @property $free_draw_count 赠送抽奖次数
* @property $total_ticket_count 总抽奖次数
* @property $paid_ticket_count 购买抽奖次数
* @property $free_ticket_count 赠送抽奖次数
* @property $remark 备注
* @property $create_time 创建时间
* @property $update_time 修改时间
@@ -79,51 +79,51 @@ class DicePlayerTicketRecord extends BaseModel
}
}
/** 总抽奖次数下限 */
public function searchTotalDrawCountMinAttr($query, $value)
/** 总抽奖次数(total_ticket_count)下限 */
public function searchTotalTicketCountMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('total_draw_count', '>=', $value);
$query->where('total_ticket_count', '>=', $value);
}
}
/** 总抽奖次数上限 */
public function searchTotalDrawCountMaxAttr($query, $value)
/** 总抽奖次数(total_ticket_count)上限 */
public function searchTotalTicketCountMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('total_draw_count', '<=', $value);
$query->where('total_ticket_count', '<=', $value);
}
}
/** 购买抽奖次数下限 */
public function searchPaidDrawCountMinAttr($query, $value)
/** 购买抽奖次数(paid_ticket_count)下限 */
public function searchPaidTicketCountMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('paid_draw_count', '>=', $value);
$query->where('paid_ticket_count', '>=', $value);
}
}
/** 购买抽奖次数上限 */
public function searchPaidDrawCountMaxAttr($query, $value)
/** 购买抽奖次数(paid_ticket_count)上限 */
public function searchPaidTicketCountMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('paid_draw_count', '<=', $value);
$query->where('paid_ticket_count', '<=', $value);
}
}
/** 赠送抽奖次数下限 */
public function searchFreeDrawCountMinAttr($query, $value)
/** 赠送抽奖次数(free_ticket_count)下限 */
public function searchFreeTicketCountMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('free_draw_count', '>=', $value);
$query->where('free_ticket_count', '>=', $value);
}
}
/** 赠送抽奖次数上限 */
public function searchFreeDrawCountMaxAttr($query, $value)
/** 赠送抽奖次数(free_ticket_count)上限 */
public function searchFreeTicketCountMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('free_draw_count', '<=', $value);
$query->where('free_ticket_count', '<=', $value);
}
}

View File

@@ -22,9 +22,9 @@ use think\model\relation\BelongsTo;
* @property $type 类型:0=充值 1=提现 2=购买抽奖次数
* @property $wallet_before 钱包操作前
* @property $wallet_after 钱包操作后
* @property $total_draw_count 总抽奖次数
* @property $paid_draw_count 购买抽奖次数
* @property $free_draw_count 赠送抽奖次数
* @property $total_ticket_count 总抽奖次数
* @property $paid_ticket_count 购买抽奖次数
* @property $free_ticket_count 赠送抽奖次数
* @property $remark 备注
* @property $user_id 操作管理员idtype 3/4 时记录)
* @property $create_time 创建时间

View File

@@ -19,9 +19,9 @@ class DicePlayerTicketRecordValidate extends BaseValidate
protected $rule = [
'player_id' => 'require',
'use_coins' => 'require',
'total_draw_count' => 'require',
'paid_draw_count' => 'require',
'free_draw_count' => 'require',
'total_ticket_count' => 'require',
'paid_ticket_count' => 'require',
'free_ticket_count' => 'require',
'remark' => 'require',
];
@@ -31,9 +31,9 @@ class DicePlayerTicketRecordValidate extends BaseValidate
protected $message = [
'player_id' => '玩家id必须填写',
'use_coins' => '消耗硬币必须填写',
'total_draw_count' => '总抽奖次数必须填写',
'paid_draw_count' => '购买抽奖次数必须填写',
'free_draw_count' => '赠送抽奖次数必须填写',
'total_ticket_count' => '总抽奖次数必须填写',
'paid_ticket_count' => '购买抽奖次数必须填写',
'free_ticket_count' => '赠送抽奖次数必须填写',
'remark' => '备注必须填写',
];
@@ -44,17 +44,17 @@ class DicePlayerTicketRecordValidate extends BaseValidate
'save' => [
'player_id',
'use_coins',
'total_draw_count',
'paid_draw_count',
'free_draw_count',
'total_ticket_count',
'paid_ticket_count',
'free_ticket_count',
'remark',
],
'update' => [
'player_id',
'use_coins',
'total_draw_count',
'paid_draw_count',
'free_draw_count',
'total_ticket_count',
'paid_ticket_count',
'free_ticket_count',
'remark',
],
];

View File

@@ -3,10 +3,18 @@
* API 鉴权与用户相关配置
*/
return [
// auth-token 签名密钥(与客户端约定,用于 /api/authToken 的 signature 校验,必填)
'auth_token_secret' => env('API_AUTH_TOKEN_SECRET', ''),
// auth-token 时间戳允许误差(秒),防重放,默认 300 秒
'auth_token_time_tolerance' => (int) env('API_AUTH_TOKEN_TIME_TOLERANCE', 300),
// auth-token 有效期(秒),默认 24 小时
'auth_token_exp' => (int) env('API_AUTH_TOKEN_EXP', 86400),
// auth-token 按设备存储的 Redis key 前缀(同一设备只保留最新一个 auth-token
'auth_token_device_prefix' => env('API_AUTH_TOKEN_DEVICE_PREFIX', 'api:auth_token:'),
// user-token 有效期(秒),默认 7 天
'user_token_exp' => (int) env('API_USER_TOKEN_EXP', 604800),
// 按用户存储当前有效 user-token 的 Redis key 前缀(同一用户仅保留最新一次登录的 token
'user_token_current_prefix' => env('API_USER_TOKEN_CURRENT_PREFIX', 'api:user:current_token:'),
// 用户信息 Redis 缓存过期时间(秒),默认 7 天
'user_cache_expire' => (int) env('API_USER_CACHE_EXPIRE', 604800),
// 用户缓存 Redis key 前缀

View File

@@ -13,22 +13,29 @@
*/
use Webman\Route;
use app\api\middleware\CheckApiAuthMiddleware;
use app\api\middleware\CheckAuthTokenMiddleware;
use app\api\middleware\CheckUserTokenMiddleware;
// API 路由:需先调用 /api/authToken 获取 auth-token请求时携带 header: auth-token 或 Authorization: Bearer <token>
// 仅需 auth-token 的路由组(authToken 接口在中间件内白名单跳过)
Route::group('/api', function () {
Route::any('/authToken', [app\api\controller\AuthTokenController::class, 'index']);
Route::post('/user/login', [app\api\controller\UserController::class, 'login']);
Route::post('/user/register', [app\api\controller\UserController::class, 'register']);
Route::post('/user/logout', [app\api\controller\UserController::class, 'logout']);
Route::get('/user/info', [app\api\controller\UserController::class, 'info']);
Route::get('/user/balance', [app\api\controller\UserController::class, 'balance']);
Route::get('/user/walletRecord', [app\api\controller\UserController::class, 'walletRecord']);
Route::get('/user/playGameRecord', [app\api\controller\UserController::class, 'playGameRecord']);
Route::post('/game/buyLotteryTickets', [app\api\controller\GameController::class, 'buyLotteryTickets']);
Route::get('/game/lotteryPool', [app\api\controller\GameController::class, 'lotteryPool']);
Route::post('/game/playStart', [app\api\controller\GameController::class, 'playStart']);
})->middleware([CheckApiAuthMiddleware::class]);
Route::any('/user/login', [app\api\controller\UserController::class, 'login']);
Route::any('/user/register', [app\api\controller\UserController::class, 'register']);
})->middleware([
CheckAuthTokenMiddleware::class,
]);
// 需 auth-token + user-token 的路由组
Route::group('/api', function () {
Route::any('/user/logout', [app\api\controller\UserController::class, 'logout']);
Route::any('/user/info', [app\api\controller\UserController::class, 'info']);
Route::any('/user/balance', [app\api\controller\UserController::class, 'balance']);
Route::any('/user/walletRecord', [app\api\controller\UserController::class, 'walletRecord']);
Route::any('/user/playGameRecord', [app\api\controller\UserController::class, 'playGameRecord']);
Route::any('/game/buyLotteryTickets', [app\api\controller\GameController::class, 'buyLotteryTickets']);
Route::any('/game/lotteryPool', [app\api\controller\GameController::class, 'lotteryPool']);
Route::any('/game/playStart', [app\api\controller\GameController::class, 'playStart']);
})->middleware([
CheckAuthTokenMiddleware::class,
CheckUserTokenMiddleware::class,
]);

View File

@@ -20,6 +20,11 @@ namespace support;
*/
class Request extends \Webman\Http\Request
{
/** 由 CheckUserTokenMiddleware 注入:当前用户 ID */
public ?int $user_id = null;
/** 由 CheckUserTokenMiddleware 注入:当前 user-token 原始字符串 */
public ?string $userToken = null;
/**
* 获取参数增强方法