Compare commits
28 Commits
878dbbf578
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e5f83846b3 | |||
| 5ab16243bd | |||
| 8d8cee696f | |||
| 74612f136e | |||
| 13d8adbfe0 | |||
| a10afa5add | |||
| 2cfbe0c80c | |||
| 85babe3fd4 | |||
| 2dcc9f479a | |||
| bff8ea04e6 | |||
| 0a3af2d422 | |||
| 21c638a231 | |||
| a6858adf14 | |||
| 5d0e2a82ff | |||
| dead78a5f3 | |||
| 0492e08cc7 | |||
| 00d964ad80 | |||
| 33e3603932 | |||
| 6d2b74a899 | |||
| 77a898df22 | |||
| ad56d6d4ce | |||
| 01f5d6c832 | |||
| 894a562eb4 | |||
| 5b39efc7a3 | |||
| 267b088242 | |||
| eaf3f2f48f | |||
| ce0af98157 | |||
| 7e3cee4150 |
@@ -20,3 +20,6 @@ VITE_OPEN_ROUTE_INFO = false
|
|||||||
|
|
||||||
# 锁屏加密密钥
|
# 锁屏加密密钥
|
||||||
VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro
|
VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro
|
||||||
|
|
||||||
|
# 登录页是否显示验证码,设为 false 可关闭(需与后端 LOGIN_CAPTCHA_ENABLE 一致)
|
||||||
|
VITE_LOGIN_CAPTCHA_ENABLED = false
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ VITE_BASE_URL = /
|
|||||||
VITE_API_URL = /api
|
VITE_API_URL = /api
|
||||||
|
|
||||||
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
|
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
|
||||||
VITE_API_PROXY_URL = http://127.0.0.1:8787
|
VITE_API_PROXY_URL = http://127.0.0.1:6688
|
||||||
|
|
||||||
# Delete console
|
# Delete console
|
||||||
VITE_DROP_CONSOLE = false
|
VITE_DROP_CONSOLE = false
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
# 【生产】环境变量
|
# 【生产】环境变量
|
||||||
|
# 前端:47.86.91.1:8866 后端:47.86.91.1:6688
|
||||||
|
|
||||||
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
|
# 应用部署基础路径:相对路径,静态资源与页面同源(从 8866 加载)
|
||||||
VITE_BASE_URL = /
|
VITE_BASE_URL = ./
|
||||||
|
|
||||||
# API 地址前缀
|
# API 根地址(后端路由为 /core、/tool、/dice 等,无 /prod 前缀,不要加 /prod)
|
||||||
VITE_API_URL = /prod
|
VITE_API_URL = https://dice-api.yuliao666.top
|
||||||
|
|
||||||
|
# 登录页是否显示验证码,设为 false 可关闭(需与后端 LOGIN_CAPTCHA_ENABLE 一致)
|
||||||
|
VITE_LOGIN_CAPTCHA_ENABLED = false
|
||||||
|
|
||||||
# Delete console
|
# Delete console
|
||||||
VITE_DROP_CONSOLE = true
|
VITE_DROP_CONSOLE = true
|
||||||
6
saiadmin-artd/src/types/api/api.d.ts
vendored
6
saiadmin-artd/src/types/api/api.d.ts
vendored
@@ -82,12 +82,12 @@ declare namespace Api {
|
|||||||
image: string
|
image: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录参数 */
|
/** 登录参数(关闭验证码时可不传 code、uuid) */
|
||||||
interface LoginParams {
|
interface LoginParams {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
code: string
|
code?: string
|
||||||
uuid: string
|
uuid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录响应 */
|
/** 登录响应 */
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ export interface EnvConfig {
|
|||||||
VITE_USE_GZIP?: string
|
VITE_USE_GZIP?: string
|
||||||
// 是否开启 CDN
|
// 是否开启 CDN
|
||||||
VITE_USE_CDN?: string
|
VITE_USE_CDN?: string
|
||||||
|
// 登录页是否启用验证码,设为 false 或 0 关闭
|
||||||
|
VITE_LOGIN_CAPTCHA_ENABLED?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用配置
|
// 应用配置
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
show-password
|
show-password
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="code">
|
<ElFormItem v-if="captchaEnabled" prop="code">
|
||||||
<ElInput
|
<ElInput
|
||||||
class="custom-height"
|
class="custom-height"
|
||||||
:placeholder="$t('login.placeholder.code')"
|
:placeholder="$t('login.placeholder.code')"
|
||||||
@@ -115,6 +115,12 @@
|
|||||||
const systemName = AppConfig.systemInfo.name
|
const systemName = AppConfig.systemInfo.name
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
/** 是否启用登录验证码(与后端 LOGIN_CAPTCHA_ENABLE 保持一致) */
|
||||||
|
const captchaEnabled = computed(() => {
|
||||||
|
const v = import.meta.env.VITE_LOGIN_CAPTCHA_ENABLED
|
||||||
|
return v !== 'false' && v !== '0'
|
||||||
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -123,16 +129,21 @@
|
|||||||
rememberPassword: true
|
rememberPassword: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = computed<FormRules>(() => ({
|
const rules = computed<FormRules>(() => {
|
||||||
username: [{ required: true, message: t('login.placeholder.username'), trigger: 'blur' }],
|
const r: FormRules = {
|
||||||
password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }],
|
username: [{ required: true, message: t('login.placeholder.username'), trigger: 'blur' }],
|
||||||
code: [{ required: true, message: t('login.placeholder.code'), trigger: 'blur' }]
|
password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }]
|
||||||
}))
|
}
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
r.code = [{ required: true, message: t('login.placeholder.code'), trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshCaptcha()
|
if (captchaEnabled.value) refreshCaptcha()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
@@ -146,12 +157,11 @@
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// 登录请求
|
// 登录请求(关闭验证码时不传 code/uuid)
|
||||||
const { access_token, refresh_token } = await fetchLogin({
|
const { access_token, refresh_token } = await fetchLogin({
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
code: formData.code,
|
...(captchaEnabled.value ? { code: formData.code, uuid: formData.uuid } : {})
|
||||||
uuid: formData.uuid
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 验证token
|
// 验证token
|
||||||
@@ -178,7 +188,7 @@
|
|||||||
console.error('[Login] Unexpected error:', error)
|
console.error('[Login] Unexpected error:', error)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
refreshCaptcha()
|
if (captchaEnabled.value) refreshCaptcha()
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import request from '@/utils/http'
|
import request from '@/utils/http'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家购买抽奖记录 API接口
|
* 抽奖券获取记录 API接口
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
list(params: Record<string, any>) {
|
list(params: Record<string, any>) {
|
||||||
return request.get<Api.Common.ApiPage>({
|
return request.get<Api.Common.ApiPage>({
|
||||||
url: '/dice/player_coin_record/DicePlayerCoinRecord/index',
|
url: '/dice/player_ticket_record/DicePlayerTicketRecord/index',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
read(id: number | string) {
|
read(id: number | string) {
|
||||||
return request.get<Api.Common.ApiData>({
|
return request.get<Api.Common.ApiData>({
|
||||||
url: '/dice/player_coin_record/DicePlayerCoinRecord/read?id=' + id
|
url: '/dice/player_ticket_record/DicePlayerTicketRecord/read?id=' + id
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
save(params: Record<string, any>) {
|
save(params: Record<string, any>) {
|
||||||
return request.post<any>({
|
return request.post<any>({
|
||||||
url: '/dice/player_coin_record/DicePlayerCoinRecord/save',
|
url: '/dice/player_ticket_record/DicePlayerTicketRecord/save',
|
||||||
data: params
|
data: params
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
update(params: Record<string, any>) {
|
update(params: Record<string, any>) {
|
||||||
return request.put<any>({
|
return request.put<any>({
|
||||||
url: '/dice/player_coin_record/DicePlayerCoinRecord/update',
|
url: '/dice/player_ticket_record/DicePlayerTicketRecord/update',
|
||||||
data: params
|
data: params
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -58,7 +58,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
delete(params: Record<string, any>) {
|
delete(params: Record<string, any>) {
|
||||||
return request.del<any>({
|
return request.del<any>({
|
||||||
url: '/dice/player_coin_record/DicePlayerCoinRecord/destroy',
|
url: '/dice/player_ticket_record/DicePlayerTicketRecord/destroy',
|
||||||
data: params
|
data: params
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -68,7 +68,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
getPlayerOptions() {
|
getPlayerOptions() {
|
||||||
return request.get<Api.Common.ApiData>({
|
return request.get<Api.Common.ApiData>({
|
||||||
url: '/dice/player_coin_record/DicePlayerCoinRecord/getPlayerOptions'
|
url: '/dice/player_ticket_record/DicePlayerTicketRecord/getPlayerOptions'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,5 +80,21 @@ export default {
|
|||||||
url: '/dice/player_wallet_record/DicePlayerWalletRecord/getPlayerWalletBefore',
|
url: '/dice/player_wallet_record/DicePlayerWalletRecord/getPlayerWalletBefore',
|
||||||
params: { player_id: playerId }
|
params: { player_id: playerId }
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员钱包操作(加点/扣点):更新玩家平台币并创建流水记录
|
||||||
|
* type 3=加点 4=扣点
|
||||||
|
*/
|
||||||
|
adminOperate(params: {
|
||||||
|
player_id: number
|
||||||
|
type: 3 | 4
|
||||||
|
coin: number
|
||||||
|
remark?: string
|
||||||
|
}) {
|
||||||
|
return request.post<any>({
|
||||||
|
url: '/dice/player_wallet_record/DicePlayerWalletRecord/adminOperate',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,18 @@
|
|||||||
{{ row.is_win === 0 ? '无' : row.is_win === 1 ? '中奖' : '-' }}
|
{{ row.is_win === 0 ? '无' : row.is_win === 1 ? '中奖' : '-' }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 方向 tag -->
|
||||||
|
<template #direction="{ row }">
|
||||||
|
<ElTag size="small" :type="row.direction === 0 ? 'primary' : 'warning'">
|
||||||
|
{{ row.direction === 0 ? '顺时针' : row.direction === 1 ? '逆时针' : '-' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
<!-- 摇取点数 tag -->
|
||||||
|
<template #roll_array="{ row }">
|
||||||
|
<ElTag size="small">
|
||||||
|
{{ formatRollArray(row.roll_array) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
<!-- 操作列 -->
|
<!-- 操作列 -->
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -106,7 +118,8 @@
|
|||||||
win_coin_min: undefined,
|
win_coin_min: undefined,
|
||||||
win_coin_max: undefined,
|
win_coin_max: undefined,
|
||||||
reward_ui_text: undefined,
|
reward_ui_text: undefined,
|
||||||
reward_tier: undefined
|
reward_tier: undefined,
|
||||||
|
direction: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理
|
||||||
@@ -122,6 +135,21 @@
|
|||||||
const rewardTierFormatter = (row: Record<string, any>) =>
|
const rewardTierFormatter = (row: Record<string, any>) =>
|
||||||
row?.diceRewardConfig?.tier ?? row?.reward_config_id ?? '-'
|
row?.diceRewardConfig?.tier ?? row?.reward_config_id ?? '-'
|
||||||
|
|
||||||
|
/** 摇取点数格式化为 1,3,4,5,6,6 */
|
||||||
|
function formatRollArray(val: unknown): string {
|
||||||
|
if (val == null || val === '') return '-'
|
||||||
|
if (Array.isArray(val)) return val.join(',')
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(val)
|
||||||
|
return Array.isArray(arr) ? arr.join(',') : val
|
||||||
|
} catch {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(val)
|
||||||
|
}
|
||||||
|
|
||||||
// 表格配置
|
// 表格配置
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
@@ -156,6 +184,10 @@
|
|||||||
{ prop: 'lottery_type', label: '抽奖类型', width: 100, useSlot: true },
|
{ prop: 'lottery_type', label: '抽奖类型', width: 100, useSlot: true },
|
||||||
{ prop: 'is_win', label: '中奖', width: 80, useSlot: true },
|
{ prop: 'is_win', label: '中奖', width: 80, useSlot: true },
|
||||||
{ prop: 'win_coin', label: '赢取平台币' },
|
{ prop: 'win_coin', label: '赢取平台币' },
|
||||||
|
{ prop: 'direction', label: '方向', width: 90, useSlot: true },
|
||||||
|
{ prop: 'start_index', label: '起始索引', width: 90 },
|
||||||
|
{ prop: 'target_index', label: '终点索引', width: 90 },
|
||||||
|
{ prop: 'roll_array', label: '摇取点数', width: 140, useSlot: true },
|
||||||
{
|
{
|
||||||
prop: 'reward_config_id',
|
prop: 'reward_config_id',
|
||||||
label: '奖励配置',
|
label: '奖励配置',
|
||||||
|
|||||||
@@ -75,6 +75,53 @@
|
|||||||
:disabled="dialogType === 'edit'"
|
:disabled="dialogType === 'edit'"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="方向" prop="direction">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.direction"
|
||||||
|
placeholder="请选择方向"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
>
|
||||||
|
<el-option label="顺时针" :value="0" />
|
||||||
|
<el-option label="逆时针" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="起始索引" prop="start_index">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.start_index"
|
||||||
|
placeholder="起始索引"
|
||||||
|
:min="0"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="终点索引" prop="target_index">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.target_index"
|
||||||
|
placeholder="终点索引"
|
||||||
|
:min="0"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="摇取点数" prop="rollArrayItems">
|
||||||
|
<div class="roll-array-wrap">
|
||||||
|
<el-input-number
|
||||||
|
v-for="(_, i) in 5"
|
||||||
|
:key="i"
|
||||||
|
v-model="formData.rollArrayItems[i]"
|
||||||
|
:min="1"
|
||||||
|
:max="6"
|
||||||
|
:precision="0"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder=""
|
||||||
|
class="roll-array-input"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="roll-array-hint">固定 5 个数,每个 1~6</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="奖励配置" prop="reward_config_id">
|
<el-form-item label="奖励配置" prop="reward_config_id">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="formData.reward_config_id"
|
v-model="formData.reward_config_id"
|
||||||
@@ -141,6 +188,23 @@
|
|||||||
lottery_type: [{ required: true, message: '请选择抽奖类型', trigger: 'change' }],
|
lottery_type: [{ required: true, message: '请选择抽奖类型', trigger: 'change' }],
|
||||||
is_win: [{ required: true, message: '请选择中奖', trigger: 'change' }],
|
is_win: [{ required: true, message: '请选择中奖', trigger: 'change' }],
|
||||||
win_coin: [{ required: true, message: '赢取平台币必填', trigger: 'blur' }],
|
win_coin: [{ required: true, message: '赢取平台币必填', trigger: 'blur' }],
|
||||||
|
rollArrayItems: [
|
||||||
|
{
|
||||||
|
validator: (_rule: any, value: (number | null)[], callback: (e?: Error) => void) => {
|
||||||
|
if (!value || value.length !== 5) {
|
||||||
|
callback(new Error('摇取点数必须为 5 个数'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ok = value.every((n) => n != null && n >= 1 && n <= 6)
|
||||||
|
if (!ok) {
|
||||||
|
callback(new Error('摇取点数必须填写 5 个数,每个 1~6'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
reward_config_id: [{ required: true, message: '请选择奖励配置', trigger: 'change' }]
|
reward_config_id: [{ required: true, message: '请选择奖励配置', trigger: 'change' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -155,10 +219,20 @@
|
|||||||
lottery_type: null as number | null,
|
lottery_type: null as number | null,
|
||||||
is_win: null as number | null,
|
is_win: null as number | null,
|
||||||
win_coin: null as number | null,
|
win_coin: null as number | null,
|
||||||
|
direction: null as number | null,
|
||||||
|
start_index: null as number | null,
|
||||||
|
target_index: null as number | null,
|
||||||
|
roll_array: null as string | number[] | null,
|
||||||
reward_config_id: null as number | null
|
reward_config_id: null as number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = reactive({ ...initialFormData })
|
/** 摇取点数固定 5 位 [n0..n4],每项 1~6 */
|
||||||
|
const rollArrayItemsDefault = (): (number | null)[] => [null, null, null, null, null]
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
...initialFormData,
|
||||||
|
rollArrayItems: rollArrayItemsDefault() as (number | null)[]
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -188,7 +262,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const initPage = async () => {
|
const initPage = async () => {
|
||||||
Object.assign(formData, { ...initialFormData })
|
Object.assign(formData, { ...initialFormData, rollArrayItems: rollArrayItemsDefault() })
|
||||||
if (props.data) {
|
if (props.data) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
initForm()
|
initForm()
|
||||||
@@ -204,16 +278,47 @@
|
|||||||
'lottery_type',
|
'lottery_type',
|
||||||
'is_win',
|
'is_win',
|
||||||
'win_coin',
|
'win_coin',
|
||||||
|
'direction',
|
||||||
|
'start_index',
|
||||||
|
'target_index',
|
||||||
|
'roll_array',
|
||||||
'reward_config_id'
|
'reward_config_id'
|
||||||
]
|
]
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
const val = props.data![key]
|
const val = props.data![key]
|
||||||
if (val != null && val !== undefined) {
|
if (val != null && val !== undefined) {
|
||||||
;(formData as Record<string, unknown>)[key] = val
|
if (key === 'roll_array') {
|
||||||
|
formData.roll_array = val
|
||||||
|
formData.rollArrayItems = parseRollArrayToItems(val)
|
||||||
|
} else {
|
||||||
|
;(formData as Record<string, unknown>)[key] = val
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将接口的 roll_array 转为固定 5 项数组,不足补 null */
|
||||||
|
function parseRollArrayToItems(val: unknown): (number | null)[] {
|
||||||
|
let arr: number[] = []
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
arr = val.map((n) => (typeof n === 'number' && !Number.isNaN(n) ? n : 0)).slice(0, 5)
|
||||||
|
} else if (typeof val === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(val)
|
||||||
|
arr = Array.isArray(parsed) ? parsed.slice(0, 5).map((n: any) => Number(n) || 0) : []
|
||||||
|
} catch {
|
||||||
|
arr = val
|
||||||
|
.split(',')
|
||||||
|
.map((n) => parseInt(n, 10))
|
||||||
|
.filter((n) => !Number.isNaN(n))
|
||||||
|
.slice(0, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const items: (number | null)[] = [...arr]
|
||||||
|
while (items.length < 5) items.push(null)
|
||||||
|
return items.slice(0, 5)
|
||||||
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
@@ -223,19 +328,56 @@
|
|||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
|
const payload = { ...formData } as Record<string, unknown>
|
||||||
|
// 将 5 个输入值拼成 [1,2,3,4,5] 格式,确保每项为 1~6 的整数
|
||||||
|
const items = formData.rollArrayItems
|
||||||
|
payload.roll_array = items.map((n) => {
|
||||||
|
const v = n != null ? Number(n) : 1
|
||||||
|
return Math.min(6, Math.max(1, Number.isNaN(v) ? 1 : Math.floor(v)))
|
||||||
|
})
|
||||||
|
delete payload.rollArrayItems
|
||||||
if (props.dialogType === 'add') {
|
if (props.dialogType === 'add') {
|
||||||
const rest = { ...formData } as Record<string, unknown>
|
delete payload.id
|
||||||
delete rest.id
|
await api.save(payload)
|
||||||
await api.save(rest)
|
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
} else {
|
} else {
|
||||||
await api.update(formData)
|
await api.update(payload)
|
||||||
ElMessage.success('修改成功')
|
ElMessage.success('修改成功')
|
||||||
}
|
}
|
||||||
emit('success')
|
emit('success')
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.log('表单验证失败:', error)
|
let msg = '表单验证失败,请检查必填项与格式'
|
||||||
|
if (error?.message) {
|
||||||
|
msg = error.message
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
msg = error
|
||||||
|
} else if (error && typeof error === 'object') {
|
||||||
|
const first = Object.values(error).find((v: any) => v?.[0]?.message)
|
||||||
|
if (first && Array.isArray(first)) {
|
||||||
|
msg = (first[0] as any).message || msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElMessage.warning(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.roll-array-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roll-array-input {
|
||||||
|
width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roll-array-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col v-bind="setSpan(6)">
|
||||||
|
<el-form-item label="方向" prop="direction">
|
||||||
|
<el-select v-model="formData.direction" placeholder="全部" clearable style="width: 100%">
|
||||||
|
<el-option label="顺时针" :value="0" />
|
||||||
|
<el-option label="逆时针" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item label="赢取平台币" prop="win_coin_min">
|
<el-form-item label="赢取平台币" prop="win_coin_min">
|
||||||
<div class="range-wrap">
|
<div class="range-wrap">
|
||||||
|
|||||||
@@ -48,12 +48,19 @@
|
|||||||
v-permission="'dice:player:index:update'"
|
v-permission="'dice:player:index:update'"
|
||||||
:model-value="row.status === 1"
|
:model-value="row.status === 1"
|
||||||
:loading="row._statusLoading"
|
:loading="row._statusLoading"
|
||||||
@change="(v: boolean) => handleStatusChange(row, v ? 1 : 0)"
|
@change="(v: string | number | boolean) => handleStatusChange(row, v ? 1 : 0)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- 平台币:tag 展示 -->
|
<!-- 平台币:tag 可点击打开钱包操作弹窗 -->
|
||||||
<template #coin="{ row }">
|
<template #coin="{ row }">
|
||||||
<ElTag type="info" size="small">{{ row.coin ?? 0 }}</ElTag>
|
<ElTag
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
class="cursor-pointer hover:opacity-80"
|
||||||
|
@click="openWalletOperate(row)"
|
||||||
|
>
|
||||||
|
{{ row.coin ?? 0 }}
|
||||||
|
</ElTag>
|
||||||
</template>
|
</template>
|
||||||
<!-- 操作列 -->
|
<!-- 操作列 -->
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
@@ -80,6 +87,13 @@
|
|||||||
:data="dialogData"
|
:data="dialogData"
|
||||||
@success="refreshData"
|
@success="refreshData"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 钱包操作弹窗(加点/扣点) -->
|
||||||
|
<WalletOperateDialog
|
||||||
|
v-model="walletDialogVisible"
|
||||||
|
:player="walletOperatePlayer"
|
||||||
|
@success="refreshData"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -89,11 +103,13 @@
|
|||||||
import api from '../../api/player/index'
|
import api from '../../api/player/index'
|
||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
|
import WalletOperateDialog from './modules/WalletOperateDialog.vue'
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
username: undefined,
|
username: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
|
phone: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
coin: undefined,
|
coin: undefined,
|
||||||
is_up: undefined
|
is_up: undefined
|
||||||
@@ -105,13 +121,23 @@
|
|||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权重列带 % 的 formatter
|
// 权重列带 % 的 formatter(ColumnOption.formatter 仅接收 row)
|
||||||
const weightFormatter = (_row: any, _column: any, cellValue: unknown) =>
|
const weightFormatter = (prop: string) => (row: any) => {
|
||||||
cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
|
const cellValue = row[prop]
|
||||||
|
return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 倍率列展示:0=正常 1=强制杀猪 2=T1高倍率
|
// 倍率列展示:0=正常 1=强制杀猪 2=T1高倍率
|
||||||
const isUpFormatter = (_row: any, _column: any, cellValue: unknown) =>
|
const isUpFormatter = (row: any) => {
|
||||||
cellValue === 0 ? '正常' : cellValue === 1 ? '强制杀猪' : cellValue === 2 ? 'T1高倍率' : '-'
|
const cellValue = row.is_up
|
||||||
|
return cellValue === 0
|
||||||
|
? '正常'
|
||||||
|
: cellValue === 1
|
||||||
|
? '强制杀猪'
|
||||||
|
: cellValue === 2
|
||||||
|
? 'T1高倍率'
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 表格配置
|
// 表格配置
|
||||||
const {
|
const {
|
||||||
@@ -133,18 +159,19 @@
|
|||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ type: 'selection' },
|
{ type: 'selection' },
|
||||||
{ prop: 'username', label: '用户名' },
|
{ prop: 'username', label: '用户名' },
|
||||||
|
{ prop: 'phone', label: '手机号' },
|
||||||
{ prop: 'name', label: '昵称' },
|
{ prop: 'name', label: '昵称' },
|
||||||
{ prop: 'status', label: '状态', width: 88, useSlot: true },
|
{ prop: 'status', label: '状态', width: 88, useSlot: true },
|
||||||
{ prop: 'coin', label: '平台币', width: 100, useSlot: true },
|
{ prop: 'coin', label: '平台币', width: 100, useSlot: true },
|
||||||
{ prop: 'is_up', label: '倍率', width: 80, formatter: isUpFormatter },
|
{ prop: 'is_up', label: '倍率', width: 80, formatter: isUpFormatter },
|
||||||
{ prop: 't1_wight', label: 'T1池权重', width: 100, formatter: weightFormatter },
|
{ prop: 't1_wight', label: 'T1池权重', width: 100, formatter: weightFormatter('t1_wight') },
|
||||||
{ prop: 't2_wight', label: 'T2池权重', width: 100, formatter: weightFormatter },
|
{ prop: 't2_wight', label: 'T2池权重', width: 100, formatter: weightFormatter('t2_wight') },
|
||||||
{ prop: 't3_wight', label: 'T3池权重', width: 100, formatter: weightFormatter },
|
{ prop: 't3_wight', label: 'T3池权重', width: 100, formatter: weightFormatter('t3_wight') },
|
||||||
{ prop: 't4_wight', label: 'T4池权重', width: 100, formatter: weightFormatter },
|
{ prop: 't4_wight', label: 'T4池权重', width: 100, formatter: weightFormatter('t4_wight') },
|
||||||
{ prop: 't5_wight', label: 'T5池权重', width: 100, formatter: weightFormatter },
|
{ prop: 't5_wight', label: 'T5池权重', width: 100, formatter: weightFormatter('t5_wight') },
|
||||||
{ prop: 'total_draw_count', label: '总抽奖次数' },
|
{ prop: 'total_ticket_count', label: '总抽奖次数' },
|
||||||
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
|
{ prop: 'paid_ticket_count', label: '购买抽奖次数' },
|
||||||
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
|
{ prop: 'free_ticket_count', label: '赠送抽奖次数' },
|
||||||
{ prop: 'created_at', label: '创建时间' },
|
{ prop: 'created_at', label: '创建时间' },
|
||||||
{ prop: 'updated_at', label: '更新时间' },
|
{ prop: 'updated_at', label: '更新时间' },
|
||||||
{ prop: 'operation', label: '操作', width: 120, fixed: 'right', useSlot: true }
|
{ prop: 'operation', label: '操作', width: 120, fixed: 'right', useSlot: true }
|
||||||
@@ -176,4 +203,18 @@
|
|||||||
handleSelectionChange,
|
handleSelectionChange,
|
||||||
selectedRows
|
selectedRows
|
||||||
} = useSaiAdmin()
|
} = useSaiAdmin()
|
||||||
|
|
||||||
|
// 钱包操作弹窗(从平台币 tag 点击打开)
|
||||||
|
const walletDialogVisible = ref(false)
|
||||||
|
type WalletPlayer = { id: number; username?: string; coin?: number }
|
||||||
|
const walletOperatePlayer = ref<WalletPlayer | null>(null)
|
||||||
|
|
||||||
|
function openWalletOperate(row: Record<string, any>) {
|
||||||
|
walletOperatePlayer.value = {
|
||||||
|
id: Number(row.id),
|
||||||
|
username: row.username,
|
||||||
|
coin: row.coin != null ? Number(row.coin) : undefined
|
||||||
|
}
|
||||||
|
walletDialogVisible.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="玩家钱包操作"
|
||||||
|
width="480px"
|
||||||
|
align-center
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="玩家">
|
||||||
|
<el-input :model-value="player?.username" disabled placeholder="-" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="钱包余额">
|
||||||
|
<el-input-number
|
||||||
|
:model-value="walletBalance"
|
||||||
|
disabled
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作类型" prop="type">
|
||||||
|
<el-select v-model="formData.type" placeholder="请选择" clearable style="width: 100%">
|
||||||
|
<el-option label="加点" :value="3" />
|
||||||
|
<el-option label="扣点" :value="4" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="平台币变动" prop="coin">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.coin"
|
||||||
|
:min="0.01"
|
||||||
|
:precision="2"
|
||||||
|
placeholder="正数,扣点时不能超过余额"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="选填,不填则按类型自动填写"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import walletRecordApi from '../../../api/player_wallet_record/index'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
interface PlayerRow {
|
||||||
|
id: number
|
||||||
|
username?: string
|
||||||
|
coin?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
player: PlayerRow | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: false,
|
||||||
|
player: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v) => emit('update:modelValue', v)
|
||||||
|
})
|
||||||
|
|
||||||
|
const walletBalance = computed(() => {
|
||||||
|
const c = props.player?.coin
|
||||||
|
return c != null ? Number(c) : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
type: [{ required: true, message: '请选择操作类型', trigger: 'change' }],
|
||||||
|
coin: [
|
||||||
|
{ required: true, message: '请输入平台币变动', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n) || n <= 0) {
|
||||||
|
callback(new Error('平台币变动必须大于 0'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (formData.type === 4 && n > walletBalance.value) {
|
||||||
|
callback(new Error('扣点不能超过当前余额'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialFormData = {
|
||||||
|
type: null as 3 | 4 | null,
|
||||||
|
coin: null as number | null,
|
||||||
|
remark: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
Object.assign(formData, initialFormData)
|
||||||
|
formData.type = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
visible.value = false
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value || !props.player?.id) return
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
const coin = Number(formData.coin) || 0
|
||||||
|
if (coin <= 0) {
|
||||||
|
ElMessage.warning('平台币变动必须大于 0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (formData.type === 4 && coin > walletBalance.value) {
|
||||||
|
ElMessage.warning('扣点不能超过当前余额')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
await walletRecordApi.adminOperate({
|
||||||
|
player_id: props.player.id,
|
||||||
|
type: formData.type!,
|
||||||
|
coin,
|
||||||
|
remark: formData.remark?.trim() || undefined
|
||||||
|
})
|
||||||
|
ElMessage.success('操作成功')
|
||||||
|
emit('success')
|
||||||
|
handleClose()
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as any)?.message) ElMessage.warning((e as any).message)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
<el-form-item label="昵称" prop="name">
|
<el-form-item label="昵称" prop="name">
|
||||||
<el-input v-model="formData.name" placeholder="请输入昵称" />
|
<el-input v-model="formData.name" placeholder="请输入昵称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="formData.phone" placeholder="请输入手机号" clearable maxlength="20" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="密码" prop="password" :rules="passwordRules">
|
<el-form-item label="密码" prop="password" :rules="passwordRules">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
@@ -117,6 +120,7 @@
|
|||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
username: [{ required: true, message: '用户名必需填写', trigger: 'blur' }],
|
username: [{ required: true, message: '用户名必需填写', trigger: 'blur' }],
|
||||||
name: [{ required: true, message: '昵称必需填写', trigger: 'blur' }],
|
name: [{ required: true, message: '昵称必需填写', trigger: 'blur' }],
|
||||||
|
phone: [{ required: true, message: '手机号必需填写', trigger: 'blur' }],
|
||||||
status: [{ required: true, message: '状态必需填写', trigger: 'blur' }],
|
status: [{ required: true, message: '状态必需填写', trigger: 'blur' }],
|
||||||
coin: [{ required: true, message: '平台币必需填写', trigger: 'blur' }]
|
coin: [{ required: true, message: '平台币必需填写', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
@@ -125,6 +129,7 @@
|
|||||||
id: null as number | null,
|
id: null as number | null,
|
||||||
username: '',
|
username: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
phone: '',
|
||||||
password: '',
|
password: '',
|
||||||
status: 1 as number,
|
status: 1 as number,
|
||||||
coin: 0 as number,
|
coin: 0 as number,
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
<el-input v-model="formData.name" placeholder="请输入昵称" clearable />
|
<el-input v-model="formData.name" placeholder="请输入昵称" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col v-bind="setSpan(6)">
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="formData.phone" placeholder="手机号模糊查询" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-select v-model="formData.status" placeholder="全部" clearable style="width: 100%">
|
<el-select v-model="formData.status" placeholder="全部" clearable style="width: 100%">
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElSpace wrap>
|
<ElSpace wrap>
|
||||||
<ElButton v-permission="'dice:player_coin_record:index:save'" @click="showDialog('add')" v-ripple>
|
<ElButton v-permission="'dice:player_ticket_record:index:save'" @click="showDialog('add')" v-ripple>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ArtSvgIcon icon="ri:add-fill" />
|
<ArtSvgIcon icon="ri:add-fill" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'dice:player_coin_record:index:destroy'"
|
v-permission="'dice:player_ticket_record:index:destroy'"
|
||||||
:disabled="selectedRows.length === 0"
|
:disabled="selectedRows.length === 0"
|
||||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||||
v-ripple
|
v-ripple
|
||||||
@@ -46,12 +46,12 @@
|
|||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<SaButton
|
<SaButton
|
||||||
v-permission="'dice:player_coin_record:index:update'"
|
v-permission="'dice:player_ticket_record:index:update'"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="showDialog('edit', row)"
|
@click="showDialog('edit', row)"
|
||||||
/>
|
/>
|
||||||
<SaButton
|
<SaButton
|
||||||
v-permission="'dice:player_coin_record:index:destroy'"
|
v-permission="'dice:player_ticket_record:index:destroy'"
|
||||||
type="error"
|
type="error"
|
||||||
@click="deleteRow(row, api.delete, refreshData)"
|
@click="deleteRow(row, api.delete, refreshData)"
|
||||||
/>
|
/>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTable } from '@/hooks/core/useTable'
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||||
import api from '../../api/player_coin_record/index'
|
import api from '../../api/player_ticket_record/index'
|
||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
|
|
||||||
@@ -83,12 +83,12 @@
|
|||||||
username: undefined,
|
username: undefined,
|
||||||
use_coins_min: undefined,
|
use_coins_min: undefined,
|
||||||
use_coins_max: undefined,
|
use_coins_max: undefined,
|
||||||
total_draw_count_min: undefined,
|
total_ticket_count_min: undefined,
|
||||||
total_draw_count_max: undefined,
|
total_ticket_count_max: undefined,
|
||||||
paid_draw_count_min: undefined,
|
paid_ticket_count_min: undefined,
|
||||||
paid_draw_count_max: undefined,
|
paid_ticket_count_max: undefined,
|
||||||
free_draw_count_min: undefined,
|
free_ticket_count_min: undefined,
|
||||||
free_draw_count_max: undefined,
|
free_ticket_count_max: undefined,
|
||||||
create_time_min: undefined,
|
create_time_min: undefined,
|
||||||
create_time_max: undefined,
|
create_time_max: undefined,
|
||||||
create_time: undefined as [string, string] | undefined
|
create_time: undefined as [string, string] | undefined
|
||||||
@@ -131,9 +131,9 @@
|
|||||||
{ prop: 'id', label: 'ID', width: 80 },
|
{ prop: 'id', label: 'ID', width: 80 },
|
||||||
{ prop: 'player_id', label: '玩家用户名', formatter: (row: Record<string, any>) => usernameFormatter(row) },
|
{ prop: 'player_id', label: '玩家用户名', formatter: (row: Record<string, any>) => usernameFormatter(row) },
|
||||||
{ prop: 'use_coins', label: '消耗硬币' },
|
{ prop: 'use_coins', label: '消耗硬币' },
|
||||||
{ prop: 'total_draw_count', label: '总抽奖次数' },
|
{ prop: 'total_ticket_count', label: '总抽奖次数' },
|
||||||
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
|
{ prop: 'paid_ticket_count', label: '购买抽奖次数' },
|
||||||
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
|
{ prop: 'free_ticket_count', label: '赠送抽奖次数' },
|
||||||
{ prop: 'remark', label: '备注', width: 100, showOverflowTooltip: true },
|
{ prop: 'remark', label: '备注', width: 100, showOverflowTooltip: true },
|
||||||
{ prop: 'create_time', label: '创建时间', width: 170 },
|
{ prop: 'create_time', label: '创建时间', width: 170 },
|
||||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogType === 'add' ? '新增玩家购买抽奖记录' : '编辑玩家购买抽奖记录'"
|
:title="dialogType === 'add' ? '新增抽奖券获取记录' : '编辑抽奖券获取记录'"
|
||||||
width="600px"
|
width="600px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@@ -28,25 +28,25 @@
|
|||||||
<el-form-item label="消耗硬币" prop="use_coins">
|
<el-form-item label="消耗硬币" prop="use_coins">
|
||||||
<el-input-number v-model="formData.use_coins" placeholder="请输入消耗硬币" :min="0" />
|
<el-input-number v-model="formData.use_coins" placeholder="请输入消耗硬币" :min="0" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="购买抽奖次数" prop="paid_draw_count">
|
<el-form-item label="购买抽奖次数" prop="paid_ticket_count">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.paid_draw_count"
|
v-model="formData.paid_ticket_count"
|
||||||
placeholder="请输入购买抽奖次数"
|
placeholder="请输入购买抽奖次数"
|
||||||
:min="0"
|
:min="0"
|
||||||
@change="onDrawCountChange"
|
@change="onTicketCountChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="赠送抽奖次数" prop="free_draw_count">
|
<el-form-item label="赠送抽奖次数" prop="free_ticket_count">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.free_draw_count"
|
v-model="formData.free_ticket_count"
|
||||||
placeholder="请输入赠送抽奖次数"
|
placeholder="请输入赠送抽奖次数"
|
||||||
:min="0"
|
:min="0"
|
||||||
@change="onDrawCountChange"
|
@change="onTicketCountChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="总抽奖次数" prop="total_draw_count">
|
<el-form-item label="总抽奖次数" prop="total_ticket_count">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
:model-value="totalDrawCountComputed"
|
:model-value="totalTicketCountComputed"
|
||||||
placeholder="自动求和"
|
placeholder="自动求和"
|
||||||
:min="0"
|
:min="0"
|
||||||
disabled
|
disabled
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/player_coin_record/index'
|
import api from '../../../api/player_ticket_record/index'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
@@ -110,23 +110,23 @@
|
|||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
|
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
|
||||||
use_coins: [{ required: true, message: '消耗硬币必需填写', trigger: 'blur' }],
|
use_coins: [{ required: true, message: '消耗硬币必需填写', trigger: 'blur' }],
|
||||||
paid_draw_count: [{ required: true, message: '购买抽奖次数必需填写', trigger: 'blur' }],
|
paid_ticket_count: [{ required: true, message: '购买抽奖次数必需填写', trigger: 'blur' }],
|
||||||
free_draw_count: [{ required: true, message: '赠送抽奖次数必需填写', trigger: 'blur' }],
|
free_ticket_count: [{ required: true, message: '赠送抽奖次数必需填写', trigger: 'blur' }],
|
||||||
remark: [{ required: true, message: '备注必需填写', trigger: 'blur' }]
|
remark: [{ required: true, message: '备注必需填写', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 玩家下拉选项(id、username) */
|
/** 玩家下拉选项(id、username) */
|
||||||
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
||||||
|
|
||||||
/** 总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数(只读展示) */
|
/** total_ticket_count = paid_ticket_count + free_ticket_count(只读展示) */
|
||||||
const totalDrawCountComputed = computed(() => {
|
const totalTicketCountComputed = computed(() => {
|
||||||
const paid = Number(formData.paid_draw_count) || 0
|
const paid = Number(formData.paid_ticket_count) || 0
|
||||||
const free = Number(formData.free_draw_count) || 0
|
const free = Number(formData.free_ticket_count) || 0
|
||||||
return paid + free
|
return paid + free
|
||||||
})
|
})
|
||||||
|
|
||||||
function onDrawCountChange() {
|
function onTicketCountChange() {
|
||||||
formData.total_draw_count = totalDrawCountComputed.value
|
formData.total_ticket_count = totalTicketCountComputed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,9 +136,9 @@
|
|||||||
id: null,
|
id: null,
|
||||||
player_id: null,
|
player_id: null,
|
||||||
use_coins: null as number | null,
|
use_coins: null as number | null,
|
||||||
total_draw_count: null as number | null,
|
total_ticket_count: null as number | null,
|
||||||
paid_draw_count: null as number | null,
|
paid_ticket_count: null as number | null,
|
||||||
free_draw_count: null as number | null,
|
free_ticket_count: null as number | null,
|
||||||
remark: ''
|
remark: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,9 +188,9 @@
|
|||||||
'id',
|
'id',
|
||||||
'player_id',
|
'player_id',
|
||||||
'use_coins',
|
'use_coins',
|
||||||
'total_draw_count',
|
'total_ticket_count',
|
||||||
'paid_draw_count',
|
'paid_ticket_count',
|
||||||
'free_draw_count',
|
'free_ticket_count',
|
||||||
'remark'
|
'remark'
|
||||||
]
|
]
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
@@ -210,12 +210,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交表单(总抽奖次数由购买+赠送自动求和,提交前写入)
|
* 提交表单(total_ticket_count 由 paid_ticket_count + free_ticket_count 自动求和,提交前写入)
|
||||||
*/
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
try {
|
try {
|
||||||
formData.total_draw_count = totalDrawCountComputed.value
|
formData.total_ticket_count = totalTicketCountComputed.value
|
||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
if (props.dialogType === 'add') {
|
if (props.dialogType === 'add') {
|
||||||
const rest = { ...formData } as Record<string, unknown>
|
const rest = { ...formData } as Record<string, unknown>
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<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">
|
<div class="range-wrap">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.total_draw_count_min"
|
v-model="formData.total_ticket_count_min"
|
||||||
placeholder="最小"
|
placeholder="最小"
|
||||||
:min="0"
|
:min="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
/>
|
/>
|
||||||
<span class="range-sep">至</span>
|
<span class="range-sep">至</span>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.total_draw_count_max"
|
v-model="formData.total_ticket_count_max"
|
||||||
placeholder="最大"
|
placeholder="最大"
|
||||||
:min="0"
|
:min="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<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">
|
<div class="range-wrap">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.paid_draw_count_min"
|
v-model="formData.paid_ticket_count_min"
|
||||||
placeholder="最小"
|
placeholder="最小"
|
||||||
:min="0"
|
:min="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
/>
|
/>
|
||||||
<span class="range-sep">至</span>
|
<span class="range-sep">至</span>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.paid_draw_count_max"
|
v-model="formData.paid_ticket_count_max"
|
||||||
placeholder="最大"
|
placeholder="最大"
|
||||||
:min="0"
|
:min="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<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">
|
<div class="range-wrap">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.free_draw_count_min"
|
v-model="formData.free_ticket_count_min"
|
||||||
placeholder="最小"
|
placeholder="最小"
|
||||||
:min="0"
|
:min="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
/>
|
/>
|
||||||
<span class="range-sep">至</span>
|
<span class="range-sep">至</span>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.free_draw_count_max"
|
v-model="formData.free_ticket_count_max"
|
||||||
placeholder="最大"
|
placeholder="最大"
|
||||||
:min="0"
|
:min="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -46,6 +46,12 @@
|
|||||||
@pagination:size-change="handleSizeChange"
|
@pagination:size-change="handleSizeChange"
|
||||||
@pagination:current-change="handleCurrentChange"
|
@pagination:current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
|
<!-- 类型:不同类型不同底色 tag,放大一倍 -->
|
||||||
|
<template #type="{ row }">
|
||||||
|
<ElTag class="wallet-type-tag" size="large" :type="typeTagType(row.type)">
|
||||||
|
{{ typeFormatter(row) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
<!-- 操作列 -->
|
<!-- 操作列 -->
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -86,18 +92,51 @@
|
|||||||
type: undefined,
|
type: undefined,
|
||||||
username: undefined,
|
username: undefined,
|
||||||
coin_min: undefined,
|
coin_min: undefined,
|
||||||
coin_max: undefined
|
coin_max: undefined,
|
||||||
|
create_time: undefined as [string, string] | undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理:将 create_time 区间转为 create_time_min / create_time_max
|
||||||
const handleSearch = (params: Record<string, any>) => {
|
const handleSearch = (params: Record<string, any>) => {
|
||||||
Object.assign(searchParams, params)
|
const p = { ...params }
|
||||||
|
if (Array.isArray(p.create_time) && p.create_time.length === 2) {
|
||||||
|
p.create_time_min = p.create_time[0]
|
||||||
|
p.create_time_max = p.create_time[1]
|
||||||
|
}
|
||||||
|
delete p.create_time
|
||||||
|
Object.assign(searchParams, p)
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 类型展示:0=充值 1=提现 2=购买抽奖次数
|
// 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 5=抽奖
|
||||||
const typeFormatter = (row: Record<string, unknown>) =>
|
const typeFormatter = (row: Record<string, unknown>) => {
|
||||||
row.type === 0 ? '充值' : row.type === 1 ? '提现' : row.type === 2 ? '购买抽奖次数' : '-'
|
const t = row.type
|
||||||
|
if (t === 0) return '充值'
|
||||||
|
if (t === 1) return '提现'
|
||||||
|
if (t === 2) return '购买抽奖次数'
|
||||||
|
if (t === 3) return '管理员加点'
|
||||||
|
if (t === 4) return '管理员扣点'
|
||||||
|
if (t === 5) return '抽奖'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型对应 tag 底色:0 充值 1 提现 2 购买 3 加点 4 扣点
|
||||||
|
const typeTagType = (t: unknown): 'success' | 'warning' | 'danger' | 'info' | 'primary' => {
|
||||||
|
if (t === 0) return 'success'
|
||||||
|
if (t === 1) return 'warning'
|
||||||
|
if (t === 2) return 'primary'
|
||||||
|
if (t === 3) return 'success'
|
||||||
|
if (t === 4) return 'danger'
|
||||||
|
if (t === 5) return 'info'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作人:关联管理员用户名
|
||||||
|
const operatorFormatter = (row: Record<string, any>) => {
|
||||||
|
const op = row.operator ?? row.operator_id
|
||||||
|
if (op && typeof op === 'object' && op.username) return op.username
|
||||||
|
return row.user_id ?? '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 用户列展示为 dicePlayer.username(兼容 dice_player)
|
// 用户列展示为 dicePlayer.username(兼容 dice_player)
|
||||||
const usernameFormatter = (row: Record<string, any>) => {
|
const usernameFormatter = (row: Record<string, any>) => {
|
||||||
@@ -123,23 +162,52 @@
|
|||||||
core: {
|
core: {
|
||||||
apiFn: api.list,
|
apiFn: api.list,
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ type: 'selection' },
|
{ type: 'selection', align: 'center' },
|
||||||
{ prop: 'id', label: 'ID', width: 80 },
|
{ prop: 'id', label: 'ID', width: 80, align: 'center' },
|
||||||
{ prop: 'player_id', label: '用户', width: 120, formatter: usernameFormatter },
|
{
|
||||||
{ prop: 'coin', label: '平台币变化', width: 110 },
|
prop: 'player_id',
|
||||||
{ prop: 'type', label: '类型', width: 120, formatter: typeFormatter },
|
label: '用户',
|
||||||
{ prop: 'wallet_before', label: '钱包操作前', width: 110 },
|
width: 120,
|
||||||
{ prop: 'wallet_after', label: '钱包操作后', width: 110 },
|
align: 'center',
|
||||||
|
formatter: usernameFormatter
|
||||||
|
},
|
||||||
|
{ prop: 'coin', label: '平台币变化', width: 110, align: 'center' },
|
||||||
|
{
|
||||||
|
prop: 'type',
|
||||||
|
label: '类型',
|
||||||
|
width: 140,
|
||||||
|
align: 'center',
|
||||||
|
useSlot: true,
|
||||||
|
formatter: typeFormatter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'user_id',
|
||||||
|
label: '操作人',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
formatter: operatorFormatter
|
||||||
|
},
|
||||||
|
{ prop: 'wallet_before', label: '钱包操作前', width: 110, align: 'center' },
|
||||||
|
{ prop: 'wallet_after', label: '钱包操作后', width: 110, align: 'center' },
|
||||||
{
|
{
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
label: '备注',
|
label: '备注',
|
||||||
width: 100,
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
showOverflowTooltip: true
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
{ prop: 'total_draw_count', label: '总抽奖次数' },
|
{ prop: 'total_ticket_count', label: '总抽奖次数', align: 'center' },
|
||||||
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
|
{ prop: 'paid_ticket_count', label: '购买抽奖次数', align: 'center' },
|
||||||
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
|
{ prop: 'free_ticket_count', label: '赠送抽奖次数', align: 'center' },
|
||||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
{ prop: 'create_time', label: '创建时间', width: 170, align: 'center' },
|
||||||
|
{
|
||||||
|
prop: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
useSlot: true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -156,3 +224,11 @@
|
|||||||
selectedRows
|
selectedRows
|
||||||
} = useSaiAdmin()
|
} = useSaiAdmin()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 类型 tag 放大一倍(large + scale) */
|
||||||
|
:deep(.wallet-record-type-tag) {
|
||||||
|
transform: scale(0.8);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
<el-option label="充值" :value="0" />
|
<el-option label="充值" :value="0" />
|
||||||
<el-option label="提现" :value="1" />
|
<el-option label="提现" :value="1" />
|
||||||
<el-option label="购买抽奖次数" :value="2" />
|
<el-option label="购买抽奖次数" :value="2" />
|
||||||
|
<el-option label="管理员加点" :value="3" />
|
||||||
|
<el-option label="管理员扣点" :value="4" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="平台币变化" prop="coin">
|
<el-form-item label="平台币变化" prop="coin">
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
<el-option label="充值" :value="0" />
|
<el-option label="充值" :value="0" />
|
||||||
<el-option label="提现" :value="1" />
|
<el-option label="提现" :value="1" />
|
||||||
<el-option label="购买抽奖次数" :value="2" />
|
<el-option label="购买抽奖次数" :value="2" />
|
||||||
|
<el-option label="管理员加点" :value="3" />
|
||||||
|
<el-option label="管理员扣点" :value="4" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -43,6 +45,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col v-bind="setSpan(8)">
|
||||||
|
<el-form-item label="创建时间" prop="create_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.create_time"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</sa-search-bar>
|
</sa-search-bar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElSpace wrap>
|
<ElSpace wrap>
|
||||||
<ElButton v-permission="'dice:reward_config:index:save'" @click="showDialog('add')" v-ripple>
|
<ElButton
|
||||||
|
v-permission="'dice:reward_config:index:save'"
|
||||||
|
@click="showDialog('add')"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ArtSvgIcon icon="ri:add-fill" />
|
<ArtSvgIcon icon="ri:add-fill" />
|
||||||
</template>
|
</template>
|
||||||
@@ -77,7 +81,6 @@
|
|||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
|
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = ref<Record<string, unknown>>({
|
const searchForm = ref<Record<string, unknown>>({
|
||||||
grid_number_min: undefined,
|
grid_number_min: undefined,
|
||||||
@@ -118,8 +121,8 @@
|
|||||||
{ prop: 'grid_number', label: '色子点数' },
|
{ prop: 'grid_number', label: '色子点数' },
|
||||||
{ prop: 'ui_text', label: '前端显示文本' },
|
{ prop: 'ui_text', label: '前端显示文本' },
|
||||||
{ prop: 'real_ev', label: '真实资金结算' },
|
{ prop: 'real_ev', label: '真实资金结算' },
|
||||||
{ prop: 'tier', label: '所属档位' },
|
{ prop: 'tier', label: '所属档位', sortable: true },
|
||||||
{ prop: 'create_time', label: '创建时间' },
|
// { prop: 'create_time', label: '创建时间', sortable: true },
|
||||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -136,5 +139,4 @@
|
|||||||
handleSelectionChange,
|
handleSelectionChange,
|
||||||
selectedRows
|
selectedRows
|
||||||
} = useSaiAdmin()
|
} = useSaiAdmin()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ DB_USER = root
|
|||||||
DB_PASSWORD = 123456
|
DB_PASSWORD = 123456
|
||||||
DB_PREFIX =
|
DB_PREFIX =
|
||||||
|
|
||||||
# 缓存方式,支持file|redis
|
# 缓存方式,支持file|redis(API 用户登录缓存需使用 redis)
|
||||||
CACHE_MODE = file
|
CACHE_MODE = redis
|
||||||
|
|
||||||
# Redis配置
|
# Redis配置
|
||||||
REDIS_HOST = 127.0.0.1
|
REDIS_HOST = 127.0.0.1
|
||||||
@@ -16,8 +16,19 @@ REDIS_PORT = 6379
|
|||||||
REDIS_PASSWORD = ''
|
REDIS_PASSWORD = ''
|
||||||
REDIS_DB = 0
|
REDIS_DB = 0
|
||||||
|
|
||||||
|
# API 鉴权与用户(可选,不填则用默认值)
|
||||||
|
# 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 = 86400
|
||||||
|
API_USER_ENCRYPT_KEY = Wj818SK8dhKBKNOY3PUTmZfhQDMCXEZi
|
||||||
|
|
||||||
# 验证码配置,支持cache|session
|
# 验证码配置,支持cache|session
|
||||||
CAPTCHA_MODE = cache
|
CAPTCHA_MODE = cache
|
||||||
|
LOGIN_CAPTCHA_ENABLE = false
|
||||||
|
|
||||||
#前端目录
|
#前端目录
|
||||||
FRONTEND_DIR = saiadmin-vue
|
FRONTEND_DIR = saiadmin-vue
|
||||||
54
server/app/api/cache/AuthTokenCache.php
vendored
Normal file
54
server/app/api/cache/AuthTokenCache.php
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
181
server/app/api/cache/UserCache.php
vendored
Normal file
181
server/app/api/cache/UserCache.php
vendored
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\cache;
|
||||||
|
|
||||||
|
use support\think\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 用户信息 Redis 缓存
|
||||||
|
* key = base64(user_id),value = 加密后的用户信息 JSON
|
||||||
|
*/
|
||||||
|
class UserCache
|
||||||
|
{
|
||||||
|
private static function prefix(): string
|
||||||
|
{
|
||||||
|
return config('api.user_cache_prefix', 'api:user:');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function expire(): int
|
||||||
|
{
|
||||||
|
return (int) config('api.user_cache_expire', 604800);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function encryptKey(): string
|
||||||
|
{
|
||||||
|
$key = config('api.user_encrypt_key', 'dafuweng_api_user_cache_key_32');
|
||||||
|
return str_pad($key, 32, '0', STR_PAD_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加密 */
|
||||||
|
public static function encrypt(string $data): string
|
||||||
|
{
|
||||||
|
$key = self::encryptKey();
|
||||||
|
$iv = substr(md5($key), 0, 16);
|
||||||
|
return base64_encode(openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解密 */
|
||||||
|
public static function decrypt(string $data): string
|
||||||
|
{
|
||||||
|
$key = self::encryptKey();
|
||||||
|
$iv = substr(md5($key), 0, 16);
|
||||||
|
$dec = openssl_decrypt(base64_decode($data, true), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
return $dec !== false ? $dec : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入用户信息到 Redis
|
||||||
|
* @param int $userId
|
||||||
|
* @param array $userInfo 从数据库读取的用户信息(可含敏感字段,会加密存储)
|
||||||
|
*/
|
||||||
|
public static function setUser(int $userId, array $userInfo): bool
|
||||||
|
{
|
||||||
|
$key = self::prefix() . base64_encode((string) $userId);
|
||||||
|
$value = self::encrypt(json_encode($userInfo));
|
||||||
|
return Cache::set($key, $value, self::expire());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Redis 读取用户信息
|
||||||
|
* @return array 解密后的用户信息,不存在或失败返回空数组
|
||||||
|
*/
|
||||||
|
public static function getUser(int $userId): array
|
||||||
|
{
|
||||||
|
$key = self::prefix() . base64_encode((string) $userId);
|
||||||
|
$value = Cache::get($key);
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$dec = self::decrypt($value);
|
||||||
|
if ($dec === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$data = json_decode($dec, true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅从缓存读取用户平台币 coin(不查库,低延迟)
|
||||||
|
* @return int|float|null 余额,缓存未命中返回 null(缓存中 coin 可能为字符串,统一转为数值)
|
||||||
|
*/
|
||||||
|
public static function getUserCoin(int $userId): int|float|null
|
||||||
|
{
|
||||||
|
$user = self::getUser($userId);
|
||||||
|
if (empty($user) || !array_key_exists('coin', $user)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$coin = $user['coin'];
|
||||||
|
if (is_int($coin) || is_float($coin)) {
|
||||||
|
return $coin;
|
||||||
|
}
|
||||||
|
if (is_string($coin) && is_numeric($coin)) {
|
||||||
|
return str_contains($coin, '.') ? (float) $coin : (int) $coin;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除用户缓存 */
|
||||||
|
public static function deleteUser(int $userId): bool
|
||||||
|
{
|
||||||
|
$key = self::prefix() . base64_encode((string) $userId);
|
||||||
|
return Cache::delete($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** user-token 黑名单前缀(退出登录后使 token 失效) */
|
||||||
|
private static function blacklistPrefix(): string
|
||||||
|
{
|
||||||
|
return config('api.user_cache_prefix', 'api:user:') . 'token_blacklist:';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 user-token 加入黑名单(退出登录)
|
||||||
|
* @param string $token 完整 token 字符串
|
||||||
|
* @param int $ttl 黑名单过期时间(秒),建议为 token 剩余有效期
|
||||||
|
*/
|
||||||
|
public static function addTokenToBlacklist(string $token, int $ttl = 86400): bool
|
||||||
|
{
|
||||||
|
if ($ttl <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$key = self::blacklistPrefix() . md5($token);
|
||||||
|
return Cache::set($key, '1', $ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 user-token 是否在黑名单中(已退出)
|
||||||
|
*/
|
||||||
|
public static function isTokenBlacklisted(string $token): bool
|
||||||
|
{
|
||||||
|
$key = self::blacklistPrefix() . md5($token);
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
server/app/api/controller/AuthTokenController.php
Normal file
90
server/app/api/controller/AuthTokenController.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
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 /api/authToken
|
||||||
|
* 参数:signature(签名)、secret(密钥)、device(设备标识)、time(时间戳,秒),四者均为必传且非空
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
server/app/api/controller/GameController.php
Normal file
120
server/app/api/controller/GameController.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
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;
|
||||||
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
|
use plugin\saiadmin\basic\OpenController;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏相关接口(购买抽奖券等)
|
||||||
|
*/
|
||||||
|
class GameController extends OpenController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 购买抽奖券
|
||||||
|
* POST /api/game/buyLotteryTickets
|
||||||
|
* header: auth-token, user-token(由 CheckUserTokenMiddleware 注入 request->user_id)
|
||||||
|
* body: count = 1 | 5 | 10(1次/100coin, 5次/500coin, 10次/1000coin)
|
||||||
|
*/
|
||||||
|
public function buyLotteryTickets(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
|
||||||
|
$count = (int) $request->post('count', 0);
|
||||||
|
if (!in_array($count, [1, 5, 10], true)) {
|
||||||
|
return $this->fail('购买抽奖券错误', ReturnCode::PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$logic = new GameLogic();
|
||||||
|
$data = $logic->buyLotteryTickets($userId, $count);
|
||||||
|
return $this->success($data);
|
||||||
|
} catch (ApiException $e) {
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
if ($msg === '平台币不足') {
|
||||||
|
$player = DicePlayer::find($userId);
|
||||||
|
$coin = $player ? (float) $player->coin : 0;
|
||||||
|
return $this->success(['coin' => $coin], $msg);
|
||||||
|
}
|
||||||
|
return $this->fail($msg, ReturnCode::BUSINESS_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取彩金池(中奖配置表)
|
||||||
|
* GET /api/game/lotteryPool
|
||||||
|
* header: auth-token
|
||||||
|
* 返回 DiceRewardConfig 列表(彩金池/中奖配置)
|
||||||
|
*/
|
||||||
|
public function lotteryPool(Request $request): Response
|
||||||
|
{
|
||||||
|
$list = DiceRewardConfig::order('id', 'asc')->select()->toArray();
|
||||||
|
return $this->success($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏(抽奖一局)
|
||||||
|
* POST /api/game/playStart
|
||||||
|
* header: auth-token, user-token(由 CheckUserTokenMiddleware 注入 request->user_id)
|
||||||
|
* body: rediction 必传,0=无 1=中奖
|
||||||
|
*/
|
||||||
|
public function playStart(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
|
||||||
|
$rediction = $request->post('rediction');
|
||||||
|
if ($rediction === '' || $rediction === null) {
|
||||||
|
return $this->fail('请传递 rediction 参数', ReturnCode::PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
$direction = (int) $rediction;
|
||||||
|
if (!in_array($direction, [0, 1], true)) {
|
||||||
|
return $this->fail('rediction 必须为 0 或 1', ReturnCode::PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = DicePlayer::find($userId);
|
||||||
|
if (!$player) {
|
||||||
|
return $this->fail('用户不存在', ReturnCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$minEv = (float) DiceRewardConfig::min('real_ev');
|
||||||
|
$minCoin = abs($minEv + 100);
|
||||||
|
$coin = (float) $player->coin;
|
||||||
|
if ($coin < $minCoin) {
|
||||||
|
return $this->success([], '当前玩家余额小于DiceRewardConfigMin.real_ev+100无法继续游戏');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$logic = new PlayStartLogic();
|
||||||
|
$data = $logic->run($userId, $direction);
|
||||||
|
return $this->success($data);
|
||||||
|
} catch (ApiException $e) {
|
||||||
|
return $this->fail($e->getMessage(), ReturnCode::BUSINESS_ERROR);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$timeoutRecord = null;
|
||||||
|
try {
|
||||||
|
$timeoutRecord = DicePlayRecord::create([
|
||||||
|
'player_id' => $userId,
|
||||||
|
'lottery_config_id' => 0,
|
||||||
|
'lottery_type' => 0,
|
||||||
|
'win_coin' => 0,
|
||||||
|
'direction' => $direction,
|
||||||
|
'reward_config_id' => 0,
|
||||||
|
'start_index' => 0,
|
||||||
|
'target_index' => 0,
|
||||||
|
'roll_array' => '[]',
|
||||||
|
'status' => PlayStartLogic::RECORD_STATUS_TIMEOUT,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $_) {
|
||||||
|
}
|
||||||
|
$payload = $timeoutRecord ? ['record' => $timeoutRecord->toArray()] : [];
|
||||||
|
return $this->success($payload, '服务超时');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
server/app/api/controller/UserController.php
Normal file
207
server/app/api/controller/UserController.php
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
use app\api\cache\UserCache;
|
||||||
|
use app\api\logic\UserLogic;
|
||||||
|
use app\api\util\ReturnCode;
|
||||||
|
use app\dice\model\play_record\DicePlayRecord;
|
||||||
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
|
use plugin\saiadmin\basic\OpenController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 用户登录/注册
|
||||||
|
* 需先携带 auth-token,登录/注册成功后返回 user-token 与用户信息,用户信息已写入 Redis(key=base64(user_id),value=加密)
|
||||||
|
*/
|
||||||
|
class UserController extends OpenController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
* POST /api/user/login
|
||||||
|
* body: phone (+60), password
|
||||||
|
*/
|
||||||
|
public function login(Request $request): Response
|
||||||
|
{
|
||||||
|
$phone = $request->post('phone', '');
|
||||||
|
$password = $request->post('password', '');
|
||||||
|
if ($phone === '' || $password === '') {
|
||||||
|
return $this->fail('请填写手机号和密码', ReturnCode::PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
$logic = new UserLogic();
|
||||||
|
$data = $logic->login($phone, $password);
|
||||||
|
return $this->success([
|
||||||
|
'user' => $data['user'],
|
||||||
|
'user-token' => $data['user-token'],
|
||||||
|
'user_id' => $data['user_id'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
* POST /api/user/register
|
||||||
|
* body: phone (+60), password, nickname(可选)
|
||||||
|
*/
|
||||||
|
public function register(Request $request): Response
|
||||||
|
{
|
||||||
|
$phone = $request->post('phone', '');
|
||||||
|
$password = $request->post('password', '');
|
||||||
|
$nickname = $request->post('nickname');
|
||||||
|
if ($phone === '' || $password === '') {
|
||||||
|
return $this->fail('请填写手机号和密码', ReturnCode::PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
$logic = new UserLogic();
|
||||||
|
$data = $logic->register($phone, $password, $nickname ? (string) $nickname : null);
|
||||||
|
return $this->success([
|
||||||
|
'user' => $data['user'],
|
||||||
|
'user-token' => $data['user-token'],
|
||||||
|
'user_id' => $data['user_id'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
* POST /api/user/logout
|
||||||
|
* header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->userToken)
|
||||||
|
*/
|
||||||
|
public function logout(Request $request): Response
|
||||||
|
{
|
||||||
|
$token = $request->userToken ?? UserLogic::getTokenFromRequest($request);
|
||||||
|
if ($token === '' || !UserLogic::logout($token)) {
|
||||||
|
return $this->fail('退出失败或 token 已失效', ReturnCode::TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
return $this->success('已退出登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
* GET /api/user/info
|
||||||
|
* header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id)
|
||||||
|
* 返回:id, username, phone, uid, name, coin, total_ticket_count
|
||||||
|
*/
|
||||||
|
public function info(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
|
||||||
|
$user = UserLogic::getCachedUser($userId);
|
||||||
|
if (empty($user)) {
|
||||||
|
return $this->fail('用户不存在', ReturnCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$fields = ['id', 'username', 'phone', 'uid', 'name', 'coin', 'total_ticket_count'];
|
||||||
|
$info = [];
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (array_key_exists($field, $user)) {
|
||||||
|
$info[$field] = $user[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->success($info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取钱包余额(优先读缓存,缓存未命中时从库拉取并回写缓存)
|
||||||
|
* GET /api/user/balance
|
||||||
|
* header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id)
|
||||||
|
*/
|
||||||
|
public function balance(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
|
||||||
|
$user = UserLogic::getCachedUser($userId);
|
||||||
|
if (empty($user)) {
|
||||||
|
return $this->fail('用户不存在', ReturnCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$coin = $user['coin'] ?? 0;
|
||||||
|
if (is_string($coin) && is_numeric($coin)) {
|
||||||
|
$coin = str_contains($coin, '.') ? (float) $coin : (int) $coin;
|
||||||
|
}
|
||||||
|
return $this->success([
|
||||||
|
'coin' => $coin,
|
||||||
|
'phone' => $user['phone'] ?? '',
|
||||||
|
'username' => $user['username'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家钱包流水
|
||||||
|
* GET /api/user/walletRecord
|
||||||
|
* header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id)
|
||||||
|
* 参数: page 页码(默认1), limit 每页条数(默认10), create_time_min/create_time_max 创建时间范围(可选)
|
||||||
|
*/
|
||||||
|
public function walletRecord(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
|
||||||
|
$page = (int) $request->post('page', 1);
|
||||||
|
$limit = (int) $request->post('limit', 10);
|
||||||
|
if ($page < 1) {
|
||||||
|
$page = 1;
|
||||||
|
}
|
||||||
|
if ($limit < 1) {
|
||||||
|
$limit = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = DicePlayerWalletRecord::where('player_id', $userId)->order('id', 'desc');
|
||||||
|
|
||||||
|
$createTimeMin = $request->post('create_time_min', '');
|
||||||
|
$createTimeMax = $request->post('create_time_max', '');
|
||||||
|
if ($createTimeMin !== '' && $createTimeMin !== null) {
|
||||||
|
$query->where('create_time', '>=', $createTimeMin);
|
||||||
|
}
|
||||||
|
if ($createTimeMax !== '' && $createTimeMax !== null) {
|
||||||
|
$query->where('create_time', '<=', $createTimeMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $query->count();
|
||||||
|
$list = $query->page($page, $limit)->select()->toArray();
|
||||||
|
$totalPage = $limit > 0 ? (int) ceil($total / $limit) : 0;
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'list' => $list,
|
||||||
|
'total_count' => $total,
|
||||||
|
'total_page' => $totalPage,
|
||||||
|
'current_page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游玩记录
|
||||||
|
* GET /api/user/playGameRecord
|
||||||
|
* header: user-token(由 CheckUserTokenMiddleware 校验并注入 request->user_id)
|
||||||
|
* 参数: page 页码(默认1), limit 每页条数(默认10), create_time_min/create_time_max 创建时间范围(可选)
|
||||||
|
*/
|
||||||
|
public function playGameRecord(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = UserLogic::getUserIdFromRequest($request) ?? 0;
|
||||||
|
$page = (int) $request->post('page', 1);
|
||||||
|
$limit = (int) $request->post('limit', 10);
|
||||||
|
if ($page < 1) {
|
||||||
|
$page = 1;
|
||||||
|
}
|
||||||
|
if ($limit < 1) {
|
||||||
|
$limit = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = DicePlayRecord::where('player_id', $userId)->order('id', 'desc');
|
||||||
|
|
||||||
|
$createTimeMin = $request->post('create_time_min', '');
|
||||||
|
$createTimeMax = $request->post('create_time_max', '');
|
||||||
|
if ($createTimeMin !== '' && $createTimeMin !== null) {
|
||||||
|
$query->where('create_time', '>=', $createTimeMin);
|
||||||
|
}
|
||||||
|
if ($createTimeMax !== '' && $createTimeMax !== null) {
|
||||||
|
$query->where('create_time', '<=', $createTimeMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $query->count();
|
||||||
|
$list = $query->page($page, $limit)->select()->toArray();
|
||||||
|
$totalPage = $limit > 0 ? (int) ceil($total / $limit) : 0;
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'list' => $list,
|
||||||
|
'total_count' => $total,
|
||||||
|
'total_page' => $totalPage,
|
||||||
|
'current_page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
server/app/api/logic/GameLogic.php
Normal file
113
server/app/api/logic/GameLogic.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\logic;
|
||||||
|
|
||||||
|
use app\api\cache\UserCache;
|
||||||
|
use app\dice\model\player\DicePlayer;
|
||||||
|
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||||
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购买抽奖券套餐:次数 => [消耗coin, 购买次数paid, 赠送次数free]
|
||||||
|
* 仅支持 1、5、10 档
|
||||||
|
*/
|
||||||
|
class GameLogic
|
||||||
|
{
|
||||||
|
public const PACKAGES = [
|
||||||
|
1 => ['coin' => 100, 'paid' => 1, 'free' => 0], // 1次/100coin
|
||||||
|
5 => ['coin' => 500, 'paid' => 5, 'free' => 1], // 5张/500coin(5购买+1赠送,共6次)
|
||||||
|
10 => ['coin' => 1000, 'paid' => 10, 'free' => 3], // 10张/1000coin(10购买+3赠送,共13次)
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 钱包流水类型:购买抽奖次数 */
|
||||||
|
public const WALLET_TYPE_BUY_DRAW = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购买抽奖券
|
||||||
|
* @param int $playerId 玩家ID(即 user_id)
|
||||||
|
* @param int $count 购买档位:1 / 5 / 10
|
||||||
|
* @return array 更新后的 coin, total_ticket_count, paid_ticket_count, free_ticket_count
|
||||||
|
*/
|
||||||
|
public function buyLotteryTickets(int $playerId, int $count): array
|
||||||
|
{
|
||||||
|
if (!isset(self::PACKAGES[$count])) {
|
||||||
|
throw new ApiException('购买抽奖券错误');
|
||||||
|
}
|
||||||
|
$pack = self::PACKAGES[$count];
|
||||||
|
$cost = $pack['coin'];
|
||||||
|
$addPaid = $pack['paid'];
|
||||||
|
$addFree = $pack['free'];
|
||||||
|
$addTotal = $addPaid + $addFree;
|
||||||
|
|
||||||
|
$player = DicePlayer::find($playerId);
|
||||||
|
if (!$player) {
|
||||||
|
throw new ApiException('用户不存在');
|
||||||
|
}
|
||||||
|
$coinBefore = (float) $player->coin;
|
||||||
|
if ($coinBefore < $cost) {
|
||||||
|
throw new ApiException('平台币不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$coinAfter = $coinBefore - $cost;
|
||||||
|
$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,
|
||||||
|
$playerId,
|
||||||
|
$cost,
|
||||||
|
$coinBefore,
|
||||||
|
$coinAfter,
|
||||||
|
$addTotal,
|
||||||
|
$addPaid,
|
||||||
|
$addFree,
|
||||||
|
$totalBefore,
|
||||||
|
$paidBefore,
|
||||||
|
$freeBefore
|
||||||
|
) {
|
||||||
|
$player->coin = $coinAfter;
|
||||||
|
$player->total_ticket_count = $totalBefore + $addTotal;
|
||||||
|
$player->paid_ticket_count = $paidBefore + $addPaid;
|
||||||
|
$player->free_ticket_count = $freeBefore + $addFree;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
// 钱包流水记录
|
||||||
|
DicePlayerWalletRecord::create([
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'coin' => -$cost,
|
||||||
|
'type' => self::WALLET_TYPE_BUY_DRAW,
|
||||||
|
'wallet_before' => $coinBefore,
|
||||||
|
'wallet_after' => $coinAfter,
|
||||||
|
'total_ticket_count' => $addTotal,
|
||||||
|
'paid_ticket_count' => $addPaid,
|
||||||
|
'free_ticket_count' => $addFree,
|
||||||
|
'remark' => "购买抽奖券{$addTotal}次(付费{$addPaid}次+赠送{$addFree}次)",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 抽奖券获取记录
|
||||||
|
DicePlayerTicketRecord::create([
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'use_coins' => $cost,
|
||||||
|
'total_ticket_count' => $addTotal,
|
||||||
|
'paid_ticket_count' => $addPaid,
|
||||||
|
'free_ticket_count' => $addFree,
|
||||||
|
'remark' => "购买抽奖券{$addTotal}次(付费{$addPaid}次+赠送{$addFree}次)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$updated = DicePlayer::find($playerId);
|
||||||
|
$userArr = $updated->hidden(['password'])->toArray();
|
||||||
|
UserCache::setUser($playerId, $userArr);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'coin' => (float) $updated->coin,
|
||||||
|
'total_ticket_count' => (int) $updated->total_ticket_count,
|
||||||
|
'paid_ticket_count' => (int) $updated->paid_ticket_count,
|
||||||
|
'free_ticket_count' => (int) $updated->free_ticket_count,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
224
server/app/api/logic/PlayStartLogic.php
Normal file
224
server/app/api/logic/PlayStartLogic.php
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\logic;
|
||||||
|
|
||||||
|
use app\api\cache\UserCache;
|
||||||
|
use app\api\service\LotteryService;
|
||||||
|
use app\dice\model\lottery_config\DiceLotteryConfig;
|
||||||
|
use app\dice\model\play_record\DicePlayRecord;
|
||||||
|
use app\dice\model\player\DicePlayer;
|
||||||
|
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||||
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
use support\think\Cache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏 / 抽奖一局
|
||||||
|
*/
|
||||||
|
class PlayStartLogic
|
||||||
|
{
|
||||||
|
/** 抽奖类型:付费 */
|
||||||
|
public const LOTTERY_TYPE_PAID = 0;
|
||||||
|
/** 抽奖类型:免费 */
|
||||||
|
public const LOTTERY_TYPE_FREE = 1;
|
||||||
|
/** 钱包流水类型:抽奖 */
|
||||||
|
public const WALLET_TYPE_DRAW = 5;
|
||||||
|
/** 对局状态:成功 */
|
||||||
|
public const RECORD_STATUS_SUCCESS = 1;
|
||||||
|
/** 对局状态:超时/失败 */
|
||||||
|
public const RECORD_STATUS_TIMEOUT = 0;
|
||||||
|
|
||||||
|
/** 开启对局最低余额 = |DiceRewardConfig 最小 real_ev + 100| */
|
||||||
|
private const MIN_COIN_EXTRA = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行一局游戏
|
||||||
|
* @param int $playerId 玩家ID
|
||||||
|
* @param int $direction 方向 0=无/顺时针 1=中奖/逆时针(前端 rediction)
|
||||||
|
* @return array 成功返回 DicePlayRecord 数据;余额不足时抛 ApiException,message 为约定文案
|
||||||
|
*/
|
||||||
|
public function run(int $playerId, int $direction): array
|
||||||
|
{
|
||||||
|
$player = DicePlayer::find($playerId);
|
||||||
|
if (!$player) {
|
||||||
|
throw new ApiException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$minEv = (float) DiceRewardConfig::min('real_ev');
|
||||||
|
$minCoin = abs($minEv + self::MIN_COIN_EXTRA);
|
||||||
|
$coin = (float) $player->coin;
|
||||||
|
if ($coin < $minCoin) {
|
||||||
|
throw new ApiException('当前玩家余额小于DiceRewardConfigMin.real_ev+100无法继续游戏');
|
||||||
|
}
|
||||||
|
|
||||||
|
$paid = (int) ($player->paid_ticket_count ?? 0);
|
||||||
|
$free = (int) ($player->free_ticket_count ?? 0);
|
||||||
|
if ($paid + $free <= 0) {
|
||||||
|
throw new ApiException('抽奖券不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lotteryService = LotteryService::getOrCreate($playerId);
|
||||||
|
$ticketType = LotteryService::drawTicketType($paid, $free);
|
||||||
|
$config = $ticketType === self::LOTTERY_TYPE_PAID
|
||||||
|
? ($lotteryService->getConfigType0Id() ? DiceLotteryConfig::find($lotteryService->getConfigType0Id()) : null)
|
||||||
|
: ($lotteryService->getConfigType1Id() ? DiceLotteryConfig::find($lotteryService->getConfigType1Id()) : null);
|
||||||
|
if (!$config) {
|
||||||
|
throw new ApiException('奖池配置不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tier = LotteryService::drawTierByWeights($config);
|
||||||
|
$rewards = DiceRewardConfig::where('tier', $tier)->select();
|
||||||
|
if ($rewards->isEmpty()) {
|
||||||
|
throw new ApiException('该档位暂无奖励配置');
|
||||||
|
}
|
||||||
|
$rewardList = $rewards->all();
|
||||||
|
$reward = $rewardList[array_rand($rewardList)];
|
||||||
|
$realEv = (float) $reward->real_ev;
|
||||||
|
$winCoin = 100 + $realEv; // 赢取平台币 = 100 + DiceRewardConfig.real_ev
|
||||||
|
$gridNumber = (int) $reward->grid_number;
|
||||||
|
$startIndex = (int) Cache::get(LotteryService::getStartIndexKey($playerId), 0);
|
||||||
|
$targetIndex = (int) $reward->id;
|
||||||
|
$rollArray = $this->generateRollArray($gridNumber);
|
||||||
|
|
||||||
|
$record = null;
|
||||||
|
$configId = (int) $config->id;
|
||||||
|
$rewardId = (int) $reward->id;
|
||||||
|
$configName = (string) ($config->name ?? '');
|
||||||
|
$isTierT5 = (string) ($reward->tier ?? '') === 'T5';
|
||||||
|
try {
|
||||||
|
Db::transaction(function () use (
|
||||||
|
$playerId,
|
||||||
|
$configId,
|
||||||
|
$rewardId,
|
||||||
|
$configName,
|
||||||
|
$ticketType,
|
||||||
|
$winCoin,
|
||||||
|
$realEv,
|
||||||
|
$direction,
|
||||||
|
$startIndex,
|
||||||
|
$targetIndex,
|
||||||
|
$rollArray,
|
||||||
|
$isTierT5,
|
||||||
|
&$record
|
||||||
|
) {
|
||||||
|
$record = DicePlayRecord::create([
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'lottery_config_id' => $configId,
|
||||||
|
'lottery_type' => $ticketType,
|
||||||
|
'win_coin' => $winCoin,
|
||||||
|
'direction' => $direction,
|
||||||
|
'reward_config_id' => $rewardId,
|
||||||
|
'start_index' => $startIndex,
|
||||||
|
'target_index' => $targetIndex,
|
||||||
|
'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray,
|
||||||
|
'lottery_name' => $configName,
|
||||||
|
'status' => self::RECORD_STATUS_SUCCESS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$p = DicePlayer::find($playerId);
|
||||||
|
if (!$p) {
|
||||||
|
throw new \RuntimeException('玩家不存在');
|
||||||
|
}
|
||||||
|
$coinBefore = (float) $p->coin;
|
||||||
|
$coinAfter = $coinBefore + $winCoin;
|
||||||
|
$p->coin = $coinAfter;
|
||||||
|
$p->total_ticket_count = max(0, (int) $p->total_ticket_count - 1);
|
||||||
|
if ($ticketType === self::LOTTERY_TYPE_PAID) {
|
||||||
|
$p->paid_ticket_count = max(0, (int) $p->paid_ticket_count - 1);
|
||||||
|
} else {
|
||||||
|
$p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若本局中奖档位为 T5,则额外赠送 1 次免费抽奖次数(总次数也 +1),并记录抽奖券获取记录
|
||||||
|
if ($isTierT5) {
|
||||||
|
$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_ticket_count' => 1,
|
||||||
|
'remark' => '中奖结果为T5',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$p->save();
|
||||||
|
|
||||||
|
// 累加彩金池盈利额度(累加值为 -real_ev)。若 dice_lottery_config 表有 ev 字段则执行
|
||||||
|
try {
|
||||||
|
DiceLotteryConfig::where('id', $configId)->update([
|
||||||
|
'ev' => Db::raw('IFNULL(ev,0) - ' . (float) $realEv),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $_) {
|
||||||
|
}
|
||||||
|
|
||||||
|
DicePlayerWalletRecord::create([
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'coin' => $winCoin,
|
||||||
|
'type' => self::WALLET_TYPE_DRAW,
|
||||||
|
'wallet_before' => $coinBefore,
|
||||||
|
'wallet_after' => $coinAfter,
|
||||||
|
'remark' => '抽奖|play_record_id=' . $record->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cache::set(LotteryService::getStartIndexKey($playerId), $targetIndex, 86400 * 30);
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
if ($record === null) {
|
||||||
|
try {
|
||||||
|
$record = DicePlayRecord::create([
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'lottery_config_id' => $configId ?? 0,
|
||||||
|
'lottery_type' => $ticketType,
|
||||||
|
'win_coin' => 0,
|
||||||
|
'direction' => $direction,
|
||||||
|
'reward_config_id' => 0,
|
||||||
|
'start_index' => $startIndex,
|
||||||
|
'target_index' => 0,
|
||||||
|
'roll_array' => '[]',
|
||||||
|
'status' => self::RECORD_STATUS_TIMEOUT,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $_) {
|
||||||
|
// 表可能无 status 字段时忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = DicePlayer::find($playerId);
|
||||||
|
if ($updated) {
|
||||||
|
UserCache::setUser($playerId, $updated->hidden(['password'])->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$record instanceof DicePlayRecord) {
|
||||||
|
throw new \RuntimeException('对局记录创建失败');
|
||||||
|
}
|
||||||
|
$arr = $record->toArray();
|
||||||
|
if (isset($arr['roll_array']) && is_string($arr['roll_array'])) {
|
||||||
|
$arr['roll_array'] = json_decode($arr['roll_array'], true) ?? [];
|
||||||
|
}
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成 5 个 1-6 的点数,和为 grid_number(5~30),严格不超范围 */
|
||||||
|
private function generateRollArray(int $gridNumber): array
|
||||||
|
{
|
||||||
|
$minSum = 5;
|
||||||
|
$maxSum = 30;
|
||||||
|
$n = max($minSum, min($maxSum, $gridNumber));
|
||||||
|
$dice = [1, 1, 1, 1, 1];
|
||||||
|
$remain = $n - 5;
|
||||||
|
while ($remain > 0) {
|
||||||
|
$i = array_rand($dice);
|
||||||
|
if ($dice[$i] < 6) {
|
||||||
|
$add = min($remain, 6 - $dice[$i]);
|
||||||
|
$dice[$i] += $add;
|
||||||
|
$remain -= $add;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shuffle($dice);
|
||||||
|
return $dice;
|
||||||
|
}
|
||||||
|
}
|
||||||
227
server/app/api/logic/UserLogic.php
Normal file
227
server/app/api/logic/UserLogic.php
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\logic;
|
||||||
|
|
||||||
|
use app\dice\model\player\DicePlayer;
|
||||||
|
use app\api\cache\UserCache;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
use Tinywan\Jwt\JwtToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 用户登录/注册逻辑(基于 DicePlayer 表)
|
||||||
|
* 手机号格式限制:+60(马来西亚)
|
||||||
|
*/
|
||||||
|
class UserLogic
|
||||||
|
{
|
||||||
|
/** 手机号正则:+60 开头,后跟 9–10 位数字(马来西亚) */
|
||||||
|
private const PHONE_REGEX = '/^\+60\d{9,10}$/';
|
||||||
|
|
||||||
|
/** 与 DicePlayerLogic 保持一致的密码盐,用于登录校验与注册写入 */
|
||||||
|
private const PASSWORD_SALT = 'dice_player_salt_2024';
|
||||||
|
|
||||||
|
/** 状态:正常 */
|
||||||
|
private const STATUS_NORMAL = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号格式校验:+60 开头
|
||||||
|
*/
|
||||||
|
public static function validatePhone(string $phone): void
|
||||||
|
{
|
||||||
|
if (!preg_match(self::PHONE_REGEX, $phone)) {
|
||||||
|
throw new ApiException('手机号格式错误,仅支持 +60 开头的马来西亚号码(如 +60123456789)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录:手机号 + 密码,返回用户信息与 user-token,并写入 Redis 缓存
|
||||||
|
*/
|
||||||
|
public function login(string $phone, string $password): array
|
||||||
|
{
|
||||||
|
self::validatePhone($phone);
|
||||||
|
|
||||||
|
$user = DicePlayer::where('phone', $phone)->find();
|
||||||
|
if (!$user) {
|
||||||
|
throw new ApiException('手机号未注册');
|
||||||
|
}
|
||||||
|
if ((int) $user->status !== self::STATUS_NORMAL) {
|
||||||
|
throw new ApiException('账号已被禁用');
|
||||||
|
}
|
||||||
|
$hashed = $this->hashPassword($password);
|
||||||
|
if ($user->password !== $hashed) {
|
||||||
|
throw new ApiException('密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userArr = $user->hidden(['password'])->toArray();
|
||||||
|
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,
|
||||||
|
'user_id' => (int) $user->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册:手机号 + 密码(+60),创建玩家并返回用户信息与 user-token,写入 Redis
|
||||||
|
*/
|
||||||
|
public function register(string $phone, string $password, ?string $nickname = null): array
|
||||||
|
{
|
||||||
|
self::validatePhone($phone);
|
||||||
|
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
throw new ApiException('密码至少 6 位');
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = DicePlayer::where('phone', $phone)->find();
|
||||||
|
if ($exists) {
|
||||||
|
throw new ApiException('该手机号已注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new DicePlayer();
|
||||||
|
$user->phone = $phone;
|
||||||
|
$user->username = $phone;
|
||||||
|
if ($nickname !== null && $nickname !== '') {
|
||||||
|
$user->name = $nickname;
|
||||||
|
}
|
||||||
|
// name 未传时由 DicePlayer::onBeforeInsert 默认设为 uid
|
||||||
|
$user->password = $this->hashPassword($password);
|
||||||
|
$user->status = self::STATUS_NORMAL;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$userArr = $user->hidden(['password'])->toArray();
|
||||||
|
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,
|
||||||
|
'user_id' => (int) $user->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 DicePlayerLogic 一致的密码加密:md5(salt . password)
|
||||||
|
*/
|
||||||
|
private function hashPassword(string $password): string
|
||||||
|
{
|
||||||
|
return md5(self::PASSWORD_SALT . $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 user-token(JWT,plat=api_user,id=user_id)
|
||||||
|
*/
|
||||||
|
private function generateUserToken(int $userId): string
|
||||||
|
{
|
||||||
|
$exp = config('api.user_token_exp', 604800);
|
||||||
|
$result = JwtToken::generateToken([
|
||||||
|
'id' => $userId,
|
||||||
|
'plat' => 'api_user',
|
||||||
|
'access_exp' => $exp,
|
||||||
|
]);
|
||||||
|
return $result['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求中解析 user-token(header: 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
|
||||||
|
*/
|
||||||
|
public static function getUserIdFromToken(string $userToken): ?int
|
||||||
|
{
|
||||||
|
if (UserCache::isTokenBlacklisted($userToken)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$decoded = JwtToken::verify(1, $userToken);
|
||||||
|
$extend = $decoded['extend'] ?? [];
|
||||||
|
if (($extend['plat'] ?? '') !== 'api_user') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$id = $extend['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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录:将当前 user-token 加入黑名单,使该 token 失效
|
||||||
|
*/
|
||||||
|
public static function logout(string $userToken): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$decoded = JwtToken::verify(1, $userToken);
|
||||||
|
$exp = (int) ($decoded['exp'] ?? 0);
|
||||||
|
$ttl = $exp > time() ? $exp - time() : 86400;
|
||||||
|
return UserCache::addTokenToBlacklist($userToken, $ttl);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Redis 获取用户信息(key = base64(user_id)),未命中则查 DicePlayer 并回写缓存
|
||||||
|
*/
|
||||||
|
public static function getCachedUser(int $userId): array
|
||||||
|
{
|
||||||
|
$cached = UserCache::getUser($userId);
|
||||||
|
if (!empty($cached)) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
$user = DicePlayer::find($userId);
|
||||||
|
if (!$user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$arr = $user->hidden(['password'])->toArray();
|
||||||
|
UserCache::setUser($userId, $arr);
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
server/app/api/middleware/CheckAuthTokenMiddleware.php
Normal file
109
server/app/api/middleware/CheckAuthTokenMiddleware.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
server/app/api/middleware/CheckUserTokenMiddleware.php
Normal file
42
server/app/api/middleware/CheckUserTokenMiddleware.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
server/app/api/service/LotteryService.php
Normal file
138
server/app/api/service/LotteryService.php
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\service;
|
||||||
|
|
||||||
|
use app\dice\model\lottery_config\DiceLotteryConfig;
|
||||||
|
use app\dice\model\player\DicePlayer;
|
||||||
|
use support\think\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 彩金池实例,按玩家权重与奖池配置创建,存 Redis 便于增删改查
|
||||||
|
*/
|
||||||
|
class LotteryService
|
||||||
|
{
|
||||||
|
private const REDIS_KEY_PREFIX = 'api:game:lottery_pool:';
|
||||||
|
private const REDIS_KEY_START_INDEX = 'api:game:start_index:';
|
||||||
|
private const EXPIRE = 86400 * 7; // 7天
|
||||||
|
|
||||||
|
private int $playerId;
|
||||||
|
private ?int $configType0Id = null;
|
||||||
|
private ?int $configType1Id = null;
|
||||||
|
/** @var array{t1_wight?:int,t2_wight?:int,t3_wight?:int,t4_wight?:int,t5_wight?:int} */
|
||||||
|
private array $playerWeights = [];
|
||||||
|
|
||||||
|
public function __construct(int $playerId)
|
||||||
|
{
|
||||||
|
$this->playerId = $playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRedisKey(int $playerId): string
|
||||||
|
{
|
||||||
|
return self::REDIS_KEY_PREFIX . $playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getStartIndexKey(int $playerId): string
|
||||||
|
{
|
||||||
|
return self::REDIS_KEY_START_INDEX . $playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 Redis 加载或根据玩家与 DiceLotteryConfig 创建并保存 */
|
||||||
|
public static function getOrCreate(int $playerId): self
|
||||||
|
{
|
||||||
|
$key = self::getRedisKey($playerId);
|
||||||
|
$cached = Cache::get($key);
|
||||||
|
if ($cached && is_string($cached)) {
|
||||||
|
$data = json_decode($cached, true);
|
||||||
|
if (is_array($data)) {
|
||||||
|
$s = new self($playerId);
|
||||||
|
$s->configType0Id = (int) ($data['config_type_0_id'] ?? 0);
|
||||||
|
$s->configType1Id = (int) ($data['config_type_1_id'] ?? 0);
|
||||||
|
$s->playerWeights = $data['player_weights'] ?? [];
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$player = DicePlayer::find($playerId);
|
||||||
|
if (!$player) {
|
||||||
|
throw new \RuntimeException('玩家不存在');
|
||||||
|
}
|
||||||
|
$config0 = DiceLotteryConfig::where('type', 0)->find();
|
||||||
|
$config1 = DiceLotteryConfig::where('type', 1)->find();
|
||||||
|
$s = new self($playerId);
|
||||||
|
$s->configType0Id = $config0 ? (int) $config0->id : null;
|
||||||
|
$s->configType1Id = $config1 ? (int) $config1->id : null;
|
||||||
|
$s->playerWeights = [
|
||||||
|
't1_wight' => (int) ($player->t1_wight ?? 0),
|
||||||
|
't2_wight' => (int) ($player->t2_wight ?? 0),
|
||||||
|
't3_wight' => (int) ($player->t3_wight ?? 0),
|
||||||
|
't4_wight' => (int) ($player->t4_wight ?? 0),
|
||||||
|
't5_wight' => (int) ($player->t5_wight ?? 0),
|
||||||
|
];
|
||||||
|
$s->save();
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$key = self::getRedisKey($this->playerId);
|
||||||
|
$data = [
|
||||||
|
'config_type_0_id' => $this->configType0Id,
|
||||||
|
'config_type_1_id' => $this->configType1Id,
|
||||||
|
'player_weights' => $this->playerWeights,
|
||||||
|
];
|
||||||
|
Cache::set($key, json_encode($data), self::EXPIRE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据奖池配置的 t1_wight..t5_wight 权重随机抽取档位 T1-T5 */
|
||||||
|
public static function drawTierByWeights(DiceLotteryConfig $config): string
|
||||||
|
{
|
||||||
|
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||||
|
$weights = [
|
||||||
|
(int) ($config->t1_wight ?? 0),
|
||||||
|
(int) ($config->t2_wight ?? 0),
|
||||||
|
(int) ($config->t3_wight ?? 0),
|
||||||
|
(int) ($config->t4_wight ?? 0),
|
||||||
|
(int) ($config->t5_wight ?? 0),
|
||||||
|
];
|
||||||
|
$total = array_sum($weights);
|
||||||
|
if ($total <= 0) {
|
||||||
|
return $tiers[array_rand($tiers)];
|
||||||
|
}
|
||||||
|
$r = mt_rand(1, $total);
|
||||||
|
$acc = 0;
|
||||||
|
foreach ($weights as $i => $w) {
|
||||||
|
$acc += $w;
|
||||||
|
if ($r <= $acc) {
|
||||||
|
return $tiers[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tiers[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 paid_ticket_count 与 free_ticket_count 权重随机抽取 0=付费 1=免费 */
|
||||||
|
public static function drawTicketType(int $paid, int $free): int
|
||||||
|
{
|
||||||
|
if ($paid <= 0 && $free <= 0) {
|
||||||
|
throw new \RuntimeException('抽奖券不足');
|
||||||
|
}
|
||||||
|
if ($paid <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ($free <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$total = $paid + $free;
|
||||||
|
$r = mt_rand(1, $total);
|
||||||
|
return $r <= $paid ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfigType0Id(): ?int
|
||||||
|
{
|
||||||
|
return $this->configType0Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfigType1Id(): ?int
|
||||||
|
{
|
||||||
|
return $this->configType1Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
server/app/api/util/ReturnCode.php
Normal file
35
server/app/api/util/ReturnCode.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 统一状态码
|
||||||
|
* 与 HTTP 语义对齐,便于前端与网关处理
|
||||||
|
*/
|
||||||
|
class ReturnCode
|
||||||
|
{
|
||||||
|
/** 200 成功 */
|
||||||
|
public const SUCCESS = 200;
|
||||||
|
|
||||||
|
/** 400 请求参数错误(缺少参数、参数无效、格式错误等) */
|
||||||
|
public const PARAMS_ERROR = 400;
|
||||||
|
|
||||||
|
/** 401 未授权(未携带 auth-token 或 user-token) */
|
||||||
|
public const UNAUTHORIZED = 401;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ class DicePlayRecordController extends BaseController
|
|||||||
['win_coin_max', ''],
|
['win_coin_max', ''],
|
||||||
['reward_ui_text', ''],
|
['reward_ui_text', ''],
|
||||||
['reward_tier', ''],
|
['reward_tier', ''],
|
||||||
|
['direction', ''],
|
||||||
]);
|
]);
|
||||||
$query = $this->logic->search($where);
|
$query = $this->logic->search($where);
|
||||||
$query->with([
|
$query->with([
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class DicePlayerController extends BaseController
|
|||||||
$where = $request->more([
|
$where = $request->more([
|
||||||
['username', ''],
|
['username', ''],
|
||||||
['name', ''],
|
['name', ''],
|
||||||
|
['phone', ''],
|
||||||
['status', ''],
|
['status', ''],
|
||||||
['coin', ''],
|
['coin', ''],
|
||||||
['is_up', ''],
|
['is_up', ''],
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
// | Author: your name
|
// | Author: your name
|
||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\controller\player_coin_record;
|
namespace app\dice\controller\player_ticket_record;
|
||||||
|
|
||||||
use plugin\saiadmin\basic\BaseController;
|
use plugin\saiadmin\basic\BaseController;
|
||||||
use app\dice\logic\player_coin_record\DicePlayerCoinRecordLogic;
|
use app\dice\logic\player_ticket_record\DicePlayerTicketRecordLogic;
|
||||||
use app\dice\validate\player_coin_record\DicePlayerCoinRecordValidate;
|
use app\dice\validate\player_ticket_record\DicePlayerTicketRecordValidate;
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use plugin\saiadmin\service\Permission;
|
use plugin\saiadmin\service\Permission;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家购买抽奖记录控制器
|
* 抽奖券获取记录控制器
|
||||||
*/
|
*/
|
||||||
class DicePlayerCoinRecordController extends BaseController
|
class DicePlayerTicketRecordController extends BaseController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 构造函数
|
* 构造函数
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->logic = new DicePlayerCoinRecordLogic();
|
$this->logic = new DicePlayerTicketRecordLogic();
|
||||||
$this->validate = new DicePlayerCoinRecordValidate;
|
$this->validate = new DicePlayerTicketRecordValidate;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,19 +34,19 @@ class DicePlayerCoinRecordController extends BaseController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('玩家购买抽奖记录列表', 'dice:player_coin_record:index:index')]
|
#[Permission('抽奖券获取记录列表', 'dice:player_ticket_record:index:index')]
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$where = $request->more([
|
$where = $request->more([
|
||||||
['username', ''],
|
['username', ''],
|
||||||
['use_coins_min', ''],
|
['use_coins_min', ''],
|
||||||
['use_coins_max', ''],
|
['use_coins_max', ''],
|
||||||
['total_draw_count_min', ''],
|
['total_ticket_count_min', ''],
|
||||||
['total_draw_count_max', ''],
|
['total_ticket_count_max', ''],
|
||||||
['paid_draw_count_min', ''],
|
['paid_ticket_count_min', ''],
|
||||||
['paid_draw_count_max', ''],
|
['paid_ticket_count_max', ''],
|
||||||
['free_draw_count_min', ''],
|
['free_ticket_count_min', ''],
|
||||||
['free_draw_count_max', ''],
|
['free_ticket_count_max', ''],
|
||||||
['create_time_min', ''],
|
['create_time_min', ''],
|
||||||
['create_time_max', ''],
|
['create_time_max', ''],
|
||||||
]);
|
]);
|
||||||
@@ -63,7 +63,7 @@ class DicePlayerCoinRecordController extends BaseController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('玩家购买抽奖记录列表', 'dice:player_coin_record:index:index')]
|
#[Permission('抽奖券获取记录列表', 'dice:player_ticket_record:index:index')]
|
||||||
public function getPlayerOptions(Request $request): Response
|
public function getPlayerOptions(Request $request): Response
|
||||||
{
|
{
|
||||||
$list = DicePlayer::field('id,username')->select();
|
$list = DicePlayer::field('id,username')->select();
|
||||||
@@ -78,7 +78,7 @@ class DicePlayerCoinRecordController extends BaseController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('玩家购买抽奖记录读取', 'dice:player_coin_record:index:read')]
|
#[Permission('抽奖券获取记录读取', 'dice:player_ticket_record:index:read')]
|
||||||
public function read(Request $request): Response
|
public function read(Request $request): Response
|
||||||
{
|
{
|
||||||
$id = $request->input('id', '');
|
$id = $request->input('id', '');
|
||||||
@@ -96,7 +96,7 @@ class DicePlayerCoinRecordController extends BaseController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('玩家购买抽奖记录添加', 'dice:player_coin_record:index:save')]
|
#[Permission('抽奖券获取记录添加', 'dice:player_ticket_record:index:save')]
|
||||||
public function save(Request $request): Response
|
public function save(Request $request): Response
|
||||||
{
|
{
|
||||||
$data = $request->post();
|
$data = $request->post();
|
||||||
@@ -114,7 +114,7 @@ class DicePlayerCoinRecordController extends BaseController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('玩家购买抽奖记录修改', 'dice:player_coin_record:index:update')]
|
#[Permission('抽奖券获取记录修改', 'dice:player_ticket_record:index:update')]
|
||||||
public function update(Request $request): Response
|
public function update(Request $request): Response
|
||||||
{
|
{
|
||||||
$data = $request->post();
|
$data = $request->post();
|
||||||
@@ -132,7 +132,7 @@ class DicePlayerCoinRecordController extends BaseController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('玩家购买抽奖记录删除', 'dice:player_coin_record:index:destroy')]
|
#[Permission('抽奖券获取记录删除', 'dice:player_ticket_record:index:destroy')]
|
||||||
public function destroy(Request $request): Response
|
public function destroy(Request $request): Response
|
||||||
{
|
{
|
||||||
$ids = $request->post('ids', '');
|
$ids = $request->post('ids', '');
|
||||||
@@ -13,6 +13,7 @@ use app\dice\model\player\DicePlayer;
|
|||||||
use plugin\saiadmin\service\Permission;
|
use plugin\saiadmin\service\Permission;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
|
use Tinywan\Jwt\JwtToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家钱包流水控制器
|
* 玩家钱包流水控制器
|
||||||
@@ -42,10 +43,13 @@ class DicePlayerWalletRecordController extends BaseController
|
|||||||
['username', ''],
|
['username', ''],
|
||||||
['coin_min', ''],
|
['coin_min', ''],
|
||||||
['coin_max', ''],
|
['coin_max', ''],
|
||||||
|
['create_time_min', ''],
|
||||||
|
['create_time_max', ''],
|
||||||
]);
|
]);
|
||||||
$query = $this->logic->search($where);
|
$query = $this->logic->search($where);
|
||||||
$query->with([
|
$query->with([
|
||||||
'dicePlayer',
|
'dicePlayer',
|
||||||
|
'operator',
|
||||||
]);
|
]);
|
||||||
$data = $this->logic->getList($query);
|
$data = $this->logic->getList($query);
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
@@ -104,6 +108,61 @@ class DicePlayerWalletRecordController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员钱包操作(加点/扣点):更新玩家平台币并创建流水记录
|
||||||
|
* @param Request $request
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
#[Permission('玩家钱包流水添加', 'dice:player_wallet_record:index:save')]
|
||||||
|
public function adminOperate(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $request->post();
|
||||||
|
$playerId = $data['player_id'] ?? null;
|
||||||
|
$type = isset($data['type']) ? (int) $data['type'] : null;
|
||||||
|
$coin = isset($data['coin']) ? (float) $data['coin'] : null;
|
||||||
|
|
||||||
|
if ($playerId === null || $playerId === '') {
|
||||||
|
return $this->fail('请选择玩家');
|
||||||
|
}
|
||||||
|
if (!in_array($type, [3, 4], true)) {
|
||||||
|
return $this->fail('操作类型必须为 3=加点 或 4=扣点');
|
||||||
|
}
|
||||||
|
if ($coin === null || $coin <= 0) {
|
||||||
|
return $this->fail('平台币变动必须大于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['player_id'] = $playerId;
|
||||||
|
$data['type'] = $type;
|
||||||
|
$data['coin'] = $coin;
|
||||||
|
$data['remark'] = $data['remark'] ?? '';
|
||||||
|
|
||||||
|
$adminId = null;
|
||||||
|
$checkAdmin = request()->header('check_admin');
|
||||||
|
if (!empty($checkAdmin['id'])) {
|
||||||
|
$adminId = (int) $checkAdmin['id'];
|
||||||
|
}
|
||||||
|
if (($adminId === null || $adminId <= 0)) {
|
||||||
|
try {
|
||||||
|
$token = JwtToken::getExtend();
|
||||||
|
if (!empty($token['id']) && ($token['plat'] ?? '') === 'saiadmin') {
|
||||||
|
$adminId = (int) $token['id'];
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// JWT 无效或未携带
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($adminId === null || $adminId <= 0) {
|
||||||
|
return $this->fail('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->logic->adminOperate($data, $adminId);
|
||||||
|
return $this->success('操作成功');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return $this->fail($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存数据
|
* 保存数据
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
|||||||
@@ -24,4 +24,36 @@ class DicePlayRecordLogic extends BaseLogic
|
|||||||
$this->model = new DicePlayRecord();
|
$this->model = new DicePlayRecord();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加前:roll_array 转为 JSON 字符串(数据库为 string 类型)
|
||||||
|
*/
|
||||||
|
public function add(array $data): mixed
|
||||||
|
{
|
||||||
|
$data = $this->normalizeRollArray($data);
|
||||||
|
return parent::add($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改前:roll_array 转为 JSON 字符串(数据库为 string 类型)
|
||||||
|
*/
|
||||||
|
public function edit($id, array $data): mixed
|
||||||
|
{
|
||||||
|
$data = $this->normalizeRollArray($data);
|
||||||
|
return parent::edit($id, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 roll_array 从数组转为 JSON 字符串
|
||||||
|
*/
|
||||||
|
private function normalizeRollArray(array $data): array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('roll_array', $data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
$val = $data['roll_array'];
|
||||||
|
if (is_array($val)) {
|
||||||
|
$data['roll_array'] = json_encode($val, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
// +----------------------------------------------------------------------
|
|
||||||
// | saiadmin [ saiadmin快速开发框架 ]
|
|
||||||
// +----------------------------------------------------------------------
|
|
||||||
// | Author: your name
|
|
||||||
// +----------------------------------------------------------------------
|
|
||||||
namespace app\dice\logic\player_coin_record;
|
|
||||||
|
|
||||||
use plugin\saiadmin\basic\think\BaseLogic;
|
|
||||||
use plugin\saiadmin\exception\ApiException;
|
|
||||||
use plugin\saiadmin\utils\Helper;
|
|
||||||
use app\dice\model\player_coin_record\DicePlayerCoinRecord;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 玩家购买抽奖记录逻辑层
|
|
||||||
*/
|
|
||||||
class DicePlayerCoinRecordLogic extends BaseLogic
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->model = new DicePlayerCoinRecord();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加前:总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数
|
|
||||||
*/
|
|
||||||
public function add(array $data): mixed
|
|
||||||
{
|
|
||||||
$data = $this->fillTotalDrawCount($data);
|
|
||||||
return parent::add($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改前:总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数
|
|
||||||
*/
|
|
||||||
public function edit($id, array $data): mixed
|
|
||||||
{
|
|
||||||
$data = $this->fillTotalDrawCount($data);
|
|
||||||
return parent::edit($id, $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fillTotalDrawCount(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;
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | Author: your name
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace app\dice\logic\player_ticket_record;
|
||||||
|
|
||||||
|
use plugin\saiadmin\basic\think\BaseLogic;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
use plugin\saiadmin\utils\Helper;
|
||||||
|
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽奖券获取记录逻辑层
|
||||||
|
*/
|
||||||
|
class DicePlayerTicketRecordLogic extends BaseLogic
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->model = new DicePlayerTicketRecord();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加前:total_ticket_count = paid_ticket_count + free_ticket_count
|
||||||
|
*/
|
||||||
|
public function add(array $data): mixed
|
||||||
|
{
|
||||||
|
$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->fillTotalTicketCount($data);
|
||||||
|
return parent::edit($id, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fillTotalTicketCount(array $data): array
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@ namespace app\dice\logic\player_wallet_record;
|
|||||||
|
|
||||||
use plugin\saiadmin\basic\think\BaseLogic;
|
use plugin\saiadmin\basic\think\BaseLogic;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use plugin\saiadmin\utils\Helper;
|
|
||||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
|
use app\dice\model\player\DicePlayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家钱包流水逻辑层
|
* 玩家钱包流水逻辑层
|
||||||
@@ -29,10 +29,63 @@ class DicePlayerWalletRecordLogic extends BaseLogic
|
|||||||
*/
|
*/
|
||||||
public function add(array $data): mixed
|
public function add(array $data): mixed
|
||||||
{
|
{
|
||||||
$data['total_draw_count'] = $data['total_draw_count'] ?? 0;
|
$data['total_ticket_count'] = $data['total_ticket_count'] ?? 0;
|
||||||
$data['paid_draw_count'] = $data['paid_draw_count'] ?? 0;
|
$data['paid_ticket_count'] = $data['paid_ticket_count'] ?? 0;
|
||||||
$data['free_draw_count'] = $data['free_draw_count'] ?? 0;
|
$data['free_ticket_count'] = $data['free_ticket_count'] ?? 0;
|
||||||
return parent::add($data);
|
return parent::add($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员钱包操作(加点/扣点):更新玩家平台币并创建流水记录
|
||||||
|
* @param array $data player_id, type (3=加点 4=扣点), coin (正数), remark (可选)
|
||||||
|
* @param int $adminId 当前管理员 id
|
||||||
|
* @return mixed
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function adminOperate(array $data, int $adminId): mixed
|
||||||
|
{
|
||||||
|
$playerId = (int) ($data['player_id'] ?? 0);
|
||||||
|
$type = (int) ($data['type'] ?? 0);
|
||||||
|
$coin = (float) ($data['coin'] ?? 0);
|
||||||
|
|
||||||
|
if ($playerId <= 0 || !in_array($type, [3, 4], true)) {
|
||||||
|
throw new ApiException('参数错误:需要有效的 player_id 和 type(3=加点,4=扣点)');
|
||||||
|
}
|
||||||
|
if ($coin <= 0) {
|
||||||
|
throw new ApiException('平台币变动必须大于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = DicePlayer::where('id', $playerId)->find();
|
||||||
|
if (!$player) {
|
||||||
|
throw new ApiException('玩家不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$walletBefore = (float) ($player['coin'] ?? 0);
|
||||||
|
if ($type === 4 && $walletBefore < $coin) {
|
||||||
|
throw new ApiException('扣点数量不能大于当前余额');
|
||||||
|
}
|
||||||
|
|
||||||
|
$walletAfter = $type === 3 ? $walletBefore + $coin : $walletBefore - $coin;
|
||||||
|
$remark = trim((string) ($data['remark'] ?? ''));
|
||||||
|
if ($remark === '') {
|
||||||
|
$remark = $type === 3 ? '管理员加点' : '管理员扣点';
|
||||||
|
}
|
||||||
|
|
||||||
|
DicePlayer::where('id', $playerId)->update(['coin' => $walletAfter]);
|
||||||
|
|
||||||
|
$record = [
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'coin' => $type === 3 ? $coin : -$coin,
|
||||||
|
'type' => $type,
|
||||||
|
'wallet_before' => $walletBefore,
|
||||||
|
'wallet_after' => $walletAfter,
|
||||||
|
'remark' => $remark,
|
||||||
|
'user_id' => $adminId,
|
||||||
|
'total_ticket_count' => 0,
|
||||||
|
'paid_ticket_count' => 0,
|
||||||
|
'free_ticket_count' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->model->create($record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,14 @@ use think\model\relation\BelongsTo;
|
|||||||
* @property $lottery_type 抽奖类型
|
* @property $lottery_type 抽奖类型
|
||||||
* @property $is_win 中奖
|
* @property $is_win 中奖
|
||||||
* @property $win_coin 赢取平台币
|
* @property $win_coin 赢取平台币
|
||||||
|
* @property $direction 方向:0=顺时针,1=逆时针
|
||||||
* @property $reward_config_id 奖励配置id
|
* @property $reward_config_id 奖励配置id
|
||||||
* @property $lottery_id 奖池
|
* @property $lottery_id 奖池
|
||||||
|
* @property $start_index 起始索引
|
||||||
|
* @property $target_index 结束索引
|
||||||
|
* @property $roll_array 摇取点数,格式:[1,2,3,4,5](5个点数)
|
||||||
* @property $lottery_name 奖池名
|
* @property $lottery_name 奖池名
|
||||||
|
* @property $status 状态:0=超时/失败 1=成功
|
||||||
* @property $create_time 创建时间
|
* @property $create_time 创建时间
|
||||||
* @property $update_time 修改时间
|
* @property $update_time 修改时间
|
||||||
*/
|
*/
|
||||||
@@ -157,4 +162,12 @@ class DicePlayRecord extends BaseModel
|
|||||||
$query->whereRaw('1=0');
|
$query->whereRaw('1=0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 方向 0=顺时针 1=逆时针 */
|
||||||
|
public function searchDirectionAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('direction', '=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
namespace app\dice\model\player;
|
namespace app\dice\model\player;
|
||||||
|
|
||||||
use plugin\saiadmin\basic\think\BaseModel;
|
use plugin\saiadmin\basic\think\BaseModel;
|
||||||
|
use app\dice\model\lottery_config\DiceLotteryConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 大富翁-玩家模型
|
* 大富翁-玩家模型
|
||||||
@@ -15,6 +16,8 @@ use plugin\saiadmin\basic\think\BaseModel;
|
|||||||
*
|
*
|
||||||
* @property $id ID
|
* @property $id ID
|
||||||
* @property $username 用户名
|
* @property $username 用户名
|
||||||
|
* @property $phone 手机
|
||||||
|
* @property $uid uid
|
||||||
* @property $name 昵称
|
* @property $name 昵称
|
||||||
* @property $password 密码
|
* @property $password 密码
|
||||||
* @property $status 状态
|
* @property $status 状态
|
||||||
@@ -25,9 +28,9 @@ use plugin\saiadmin\basic\think\BaseModel;
|
|||||||
* @property $t3_wight T3池权重
|
* @property $t3_wight T3池权重
|
||||||
* @property $t4_wight T4池权重
|
* @property $t4_wight T4池权重
|
||||||
* @property $t5_wight T5池权重
|
* @property $t5_wight T5池权重
|
||||||
* @property $total_draw_count 总抽奖次数
|
* @property $total_ticket_count 总抽奖次数
|
||||||
* @property $paid_draw_count 购买抽奖次数
|
* @property $paid_ticket_count 购买抽奖次数
|
||||||
* @property $free_draw_count 赠送抽奖次数
|
* @property $free_ticket_count 赠送抽奖次数
|
||||||
* @property $created_at 创建时间
|
* @property $created_at 创建时间
|
||||||
* @property $updated_at 更新时间
|
* @property $updated_at 更新时间
|
||||||
* @property $deleted_at 删除时间
|
* @property $deleted_at 删除时间
|
||||||
@@ -46,6 +49,68 @@ class DicePlayer extends BaseModel
|
|||||||
*/
|
*/
|
||||||
protected $table = 'dice_player';
|
protected $table = 'dice_player';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增前:生成唯一 uid,昵称 name 默认使用 uid
|
||||||
|
* 用 try-catch 避免表尚未含 uid 时 getAttr/getData 抛 InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public static function onBeforeInsert($model): void
|
||||||
|
{
|
||||||
|
parent::onBeforeInsert($model);
|
||||||
|
try {
|
||||||
|
$uid = $model->getAttr('uid');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$uid = null;
|
||||||
|
}
|
||||||
|
if ($uid === null || $uid === '') {
|
||||||
|
$uid = self::generateUid();
|
||||||
|
$model->setAttr('uid', $uid);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$name = $model->getAttr('name');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$name = null;
|
||||||
|
}
|
||||||
|
if ($name === null || $name === '') {
|
||||||
|
$model->setAttr('name', $uid);
|
||||||
|
}
|
||||||
|
// 彩金池权重默认取 type=0 的奖池配置
|
||||||
|
self::setDefaultWeightsFromLotteryConfig($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 DiceLotteryConfig type=0 取 t1_wight~t5_wight 作为玩家未设置时的默认值
|
||||||
|
*/
|
||||||
|
protected static function setDefaultWeightsFromLotteryConfig(DicePlayer $model): void
|
||||||
|
{
|
||||||
|
$config = DiceLotteryConfig::where('type', 0)->find();
|
||||||
|
if (!$config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$fields = ['t1_wight', 't2_wight', 't3_wight', 't4_wight', 't5_wight'];
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
try {
|
||||||
|
$val = $model->getAttr($field);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$val = null;
|
||||||
|
}
|
||||||
|
if ($val === null || $val === '') {
|
||||||
|
try {
|
||||||
|
$model->setAttr($field, $config->getAttr($field));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 忽略字段不存在
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一标识 uid(12 位十六进制)
|
||||||
|
*/
|
||||||
|
public static function generateUid(): string
|
||||||
|
{
|
||||||
|
return strtoupper(substr(bin2hex(random_bytes(6)), 0, 12));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户名 搜索
|
* 用户名 搜索
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +127,16 @@ class DicePlayer extends BaseModel
|
|||||||
$query->where('name', 'like', '%'.$value.'%');
|
$query->where('name', 'like', '%'.$value.'%');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号 模糊搜索
|
||||||
|
*/
|
||||||
|
public function searchPhoneAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('phone', 'like', '%' . $value . '%');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态 搜索
|
* 状态 搜索
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
// | Author: your name
|
// | Author: your name
|
||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\model\player_coin_record;
|
namespace app\dice\model\player_ticket_record;
|
||||||
|
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use plugin\saiadmin\basic\think\BaseModel;
|
use plugin\saiadmin\basic\think\BaseModel;
|
||||||
use think\model\relation\BelongsTo;
|
use think\model\relation\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家购买抽奖记录模型
|
* 抽奖券获取记录模型
|
||||||
*
|
*
|
||||||
* dice_player_coin_record 玩家购买抽奖记录
|
* dice_player_ticket_record 抽奖券获取记录
|
||||||
*
|
*
|
||||||
* @property $id ID
|
* @property $id ID
|
||||||
* @property $player_id 玩家id
|
* @property $player_id 玩家id
|
||||||
* @property $use_coins 消耗硬币
|
* @property $use_coins 消耗硬币
|
||||||
* @property $total_draw_count 总抽奖次数
|
* @property $total_ticket_count 总抽奖次数
|
||||||
* @property $paid_draw_count 购买抽奖次数
|
* @property $paid_ticket_count 购买抽奖次数
|
||||||
* @property $free_draw_count 赠送抽奖次数
|
* @property $free_ticket_count 赠送抽奖次数
|
||||||
* @property $remark 备注
|
* @property $remark 备注
|
||||||
* @property $create_time 创建时间
|
* @property $create_time 创建时间
|
||||||
* @property $update_time 修改时间
|
* @property $update_time 修改时间
|
||||||
*/
|
*/
|
||||||
class DicePlayerCoinRecord extends BaseModel
|
class DicePlayerTicketRecord extends BaseModel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 数据表主键
|
* 数据表主键
|
||||||
@@ -37,7 +37,7 @@ class DicePlayerCoinRecord extends BaseModel
|
|||||||
* 数据库表名称
|
* 数据库表名称
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $table = 'dice_player_coin_record';
|
protected $table = 'dice_player_ticket_record';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关联模型 dicePlayer
|
* 关联模型 dicePlayer
|
||||||
@@ -79,51 +79,51 @@ class DicePlayerCoinRecord extends BaseModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 总抽奖次数下限 */
|
/** 总抽奖次数(total_ticket_count)下限 */
|
||||||
public function searchTotalDrawCountMinAttr($query, $value)
|
public function searchTotalTicketCountMinAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$query->where('total_draw_count', '>=', $value);
|
$query->where('total_ticket_count', '>=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 总抽奖次数上限 */
|
/** 总抽奖次数(total_ticket_count)上限 */
|
||||||
public function searchTotalDrawCountMaxAttr($query, $value)
|
public function searchTotalTicketCountMaxAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$query->where('total_draw_count', '<=', $value);
|
$query->where('total_ticket_count', '<=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 购买抽奖次数下限 */
|
/** 购买抽奖次数(paid_ticket_count)下限 */
|
||||||
public function searchPaidDrawCountMinAttr($query, $value)
|
public function searchPaidTicketCountMinAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$query->where('paid_draw_count', '>=', $value);
|
$query->where('paid_ticket_count', '>=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 购买抽奖次数上限 */
|
/** 购买抽奖次数(paid_ticket_count)上限 */
|
||||||
public function searchPaidDrawCountMaxAttr($query, $value)
|
public function searchPaidTicketCountMaxAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$query->where('paid_draw_count', '<=', $value);
|
$query->where('paid_ticket_count', '<=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 赠送抽奖次数下限 */
|
/** 赠送抽奖次数(free_ticket_count)下限 */
|
||||||
public function searchFreeDrawCountMinAttr($query, $value)
|
public function searchFreeTicketCountMinAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$query->where('free_draw_count', '>=', $value);
|
$query->where('free_ticket_count', '>=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 赠送抽奖次数上限 */
|
/** 赠送抽奖次数(free_ticket_count)上限 */
|
||||||
public function searchFreeDrawCountMaxAttr($query, $value)
|
public function searchFreeTicketCountMaxAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$query->where('free_draw_count', '<=', $value);
|
$query->where('free_ticket_count', '<=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ namespace app\dice\model\player_wallet_record;
|
|||||||
|
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use plugin\saiadmin\basic\think\BaseModel;
|
use plugin\saiadmin\basic\think\BaseModel;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemUser;
|
||||||
|
use think\model\relation\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家钱包流水模型
|
* 玩家钱包流水模型
|
||||||
@@ -20,10 +22,11 @@ use plugin\saiadmin\basic\think\BaseModel;
|
|||||||
* @property $type 类型:0=充值 1=提现 2=购买抽奖次数
|
* @property $type 类型:0=充值 1=提现 2=购买抽奖次数
|
||||||
* @property $wallet_before 钱包操作前
|
* @property $wallet_before 钱包操作前
|
||||||
* @property $wallet_after 钱包操作后
|
* @property $wallet_after 钱包操作后
|
||||||
* @property $total_draw_count 总抽奖次数
|
* @property $total_ticket_count 总抽奖次数
|
||||||
* @property $paid_draw_count 购买抽奖次数
|
* @property $paid_ticket_count 购买抽奖次数
|
||||||
* @property $free_draw_count 赠送抽奖次数
|
* @property $free_ticket_count 赠送抽奖次数
|
||||||
* @property $remark 备注
|
* @property $remark 备注
|
||||||
|
* @property $user_id 操作管理员id(type 3/4 时记录)
|
||||||
* @property $create_time 创建时间
|
* @property $create_time 创建时间
|
||||||
* @property $update_time 修改时间
|
* @property $update_time 修改时间
|
||||||
*/
|
*/
|
||||||
@@ -44,11 +47,19 @@ class DicePlayerWalletRecord extends BaseModel
|
|||||||
/**
|
/**
|
||||||
* 关联模型 dicePlayer
|
* 关联模型 dicePlayer
|
||||||
*/
|
*/
|
||||||
public function dicePlayer(): \think\model\relation\BelongsTo
|
public function dicePlayer(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(DicePlayer::class, 'player_id', 'id');
|
return $this->belongsTo(DicePlayer::class, 'player_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联操作管理员(type 3/4 时有值)
|
||||||
|
*/
|
||||||
|
public function operator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SystemUser::class, 'user_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类型 搜索
|
* 类型 搜索
|
||||||
*/
|
*/
|
||||||
@@ -94,4 +105,24 @@ class DicePlayerWalletRecord extends BaseModel
|
|||||||
$query->where('coin', '<=', $value);
|
$query->where('coin', '<=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间起始 搜索
|
||||||
|
*/
|
||||||
|
public function searchCreateTimeMinAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('create_time', '>=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间结束 搜索
|
||||||
|
*/
|
||||||
|
public function searchCreateTimeMaxAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('create_time', '<=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class DicePlayRecordValidate extends BaseValidate
|
|||||||
'is_win' => 'require',
|
'is_win' => 'require',
|
||||||
'win_coin' => 'require',
|
'win_coin' => 'require',
|
||||||
'reward_config_id' => 'require',
|
'reward_config_id' => 'require',
|
||||||
|
'roll_array' => 'require|checkRollArray',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +36,7 @@ class DicePlayRecordValidate extends BaseValidate
|
|||||||
'is_win' => '中奖必须填写',
|
'is_win' => '中奖必须填写',
|
||||||
'win_coin' => '赢取平台币必须填写',
|
'win_coin' => '赢取平台币必须填写',
|
||||||
'reward_config_id' => '奖励配置必须填写',
|
'reward_config_id' => '奖励配置必须填写',
|
||||||
|
'roll_array.require' => '摇取点数必须填写',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +50,7 @@ class DicePlayRecordValidate extends BaseValidate
|
|||||||
'is_win',
|
'is_win',
|
||||||
'win_coin',
|
'win_coin',
|
||||||
'reward_config_id',
|
'reward_config_id',
|
||||||
|
'roll_array',
|
||||||
],
|
],
|
||||||
'update' => [
|
'update' => [
|
||||||
'player_id',
|
'player_id',
|
||||||
@@ -56,7 +59,36 @@ class DicePlayRecordValidate extends BaseValidate
|
|||||||
'is_win',
|
'is_win',
|
||||||
'win_coin',
|
'win_coin',
|
||||||
'reward_config_id',
|
'reward_config_id',
|
||||||
|
'roll_array',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 roll_array:必须为 5 个元素,每个值在 1~6 之间
|
||||||
|
* @param mixed $value
|
||||||
|
* @param mixed $rule
|
||||||
|
* @param array $data
|
||||||
|
* @param string $field
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
protected function checkRollArray($value, $rule = '', array $data = [], string $field = '')
|
||||||
|
{
|
||||||
|
if (is_string($value)) {
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
$value = is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
return '摇取点数必须为数组';
|
||||||
|
}
|
||||||
|
if (count($value) !== 5) {
|
||||||
|
return '摇取点数必须为 5 个数';
|
||||||
|
}
|
||||||
|
foreach ($value as $i => $n) {
|
||||||
|
$v = is_numeric($n) ? (int) $n : null;
|
||||||
|
if ($v === null || $v < 1 || $v > 6) {
|
||||||
|
return '摇取点数第' . ($i + 1) . '个值必须在 1~6 之间';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class DicePlayerValidate extends BaseValidate
|
|||||||
protected $rule = [
|
protected $rule = [
|
||||||
'username' => 'require',
|
'username' => 'require',
|
||||||
'name' => 'require',
|
'name' => 'require',
|
||||||
|
'phone' => 'require',
|
||||||
'password' => 'require',
|
'password' => 'require',
|
||||||
'status' => 'require',
|
'status' => 'require',
|
||||||
'coin' => 'require',
|
'coin' => 'require',
|
||||||
@@ -30,6 +31,7 @@ class DicePlayerValidate extends BaseValidate
|
|||||||
protected $message = [
|
protected $message = [
|
||||||
'username' => '用户名必须填写',
|
'username' => '用户名必须填写',
|
||||||
'name' => '昵称必须填写',
|
'name' => '昵称必须填写',
|
||||||
|
'phone' => '手机号必须填写',
|
||||||
'password' => '密码必须填写',
|
'password' => '密码必须填写',
|
||||||
'status' => '状态必须填写',
|
'status' => '状态必须填写',
|
||||||
'coin' => '平台币必须填写',
|
'coin' => '平台币必须填写',
|
||||||
@@ -42,6 +44,7 @@ class DicePlayerValidate extends BaseValidate
|
|||||||
'save' => [
|
'save' => [
|
||||||
'username',
|
'username',
|
||||||
'name',
|
'name',
|
||||||
|
'phone',
|
||||||
'password',
|
'password',
|
||||||
'status',
|
'status',
|
||||||
'coin',
|
'coin',
|
||||||
@@ -49,6 +52,7 @@ class DicePlayerValidate extends BaseValidate
|
|||||||
'update' => [
|
'update' => [
|
||||||
'username',
|
'username',
|
||||||
'name',
|
'name',
|
||||||
|
'phone',
|
||||||
'status',
|
'status',
|
||||||
'coin',
|
'coin',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
// | Author: your name
|
// | Author: your name
|
||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\validate\player_coin_record;
|
namespace app\dice\validate\player_ticket_record;
|
||||||
|
|
||||||
use plugin\saiadmin\basic\BaseValidate;
|
use plugin\saiadmin\basic\BaseValidate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家购买抽奖记录验证器
|
* 抽奖券获取记录验证器
|
||||||
*/
|
*/
|
||||||
class DicePlayerCoinRecordValidate extends BaseValidate
|
class DicePlayerTicketRecordValidate extends BaseValidate
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 定义验证规则
|
* 定义验证规则
|
||||||
@@ -19,9 +19,9 @@ class DicePlayerCoinRecordValidate extends BaseValidate
|
|||||||
protected $rule = [
|
protected $rule = [
|
||||||
'player_id' => 'require',
|
'player_id' => 'require',
|
||||||
'use_coins' => 'require',
|
'use_coins' => 'require',
|
||||||
'total_draw_count' => 'require',
|
'total_ticket_count' => 'require',
|
||||||
'paid_draw_count' => 'require',
|
'paid_ticket_count' => 'require',
|
||||||
'free_draw_count' => 'require',
|
'free_ticket_count' => 'require',
|
||||||
'remark' => 'require',
|
'remark' => 'require',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -31,9 +31,9 @@ class DicePlayerCoinRecordValidate extends BaseValidate
|
|||||||
protected $message = [
|
protected $message = [
|
||||||
'player_id' => '玩家id必须填写',
|
'player_id' => '玩家id必须填写',
|
||||||
'use_coins' => '消耗硬币必须填写',
|
'use_coins' => '消耗硬币必须填写',
|
||||||
'total_draw_count' => '总抽奖次数必须填写',
|
'total_ticket_count' => '总抽奖次数必须填写',
|
||||||
'paid_draw_count' => '购买抽奖次数必须填写',
|
'paid_ticket_count' => '购买抽奖次数必须填写',
|
||||||
'free_draw_count' => '赠送抽奖次数必须填写',
|
'free_ticket_count' => '赠送抽奖次数必须填写',
|
||||||
'remark' => '备注必须填写',
|
'remark' => '备注必须填写',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -44,17 +44,17 @@ class DicePlayerCoinRecordValidate extends BaseValidate
|
|||||||
'save' => [
|
'save' => [
|
||||||
'player_id',
|
'player_id',
|
||||||
'use_coins',
|
'use_coins',
|
||||||
'total_draw_count',
|
'total_ticket_count',
|
||||||
'paid_draw_count',
|
'paid_ticket_count',
|
||||||
'free_draw_count',
|
'free_ticket_count',
|
||||||
'remark',
|
'remark',
|
||||||
],
|
],
|
||||||
'update' => [
|
'update' => [
|
||||||
'player_id',
|
'player_id',
|
||||||
'use_coins',
|
'use_coins',
|
||||||
'total_draw_count',
|
'total_ticket_count',
|
||||||
'paid_draw_count',
|
'paid_ticket_count',
|
||||||
'free_draw_count',
|
'free_ticket_count',
|
||||||
'remark',
|
'remark',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
24
server/config/api.php
Normal file
24
server/config/api.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 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 前缀
|
||||||
|
'user_cache_prefix' => env('API_USER_CACHE_PREFIX', 'api:user:'),
|
||||||
|
// 用户信息加密密钥(用于 Redis 中 value 的加密),建议 32 位
|
||||||
|
'user_encrypt_key' => env('API_USER_ENCRYPT_KEY', 'dafuweng_api_user_cache_key_32'),
|
||||||
|
];
|
||||||
@@ -22,7 +22,7 @@ return [
|
|||||||
'dirname' => function () {
|
'dirname' => function () {
|
||||||
return date('Ymd');
|
return date('Ymd');
|
||||||
},
|
},
|
||||||
'domain' => 'http://127.0.0.1:8787',
|
'domain' => 'http://127.0.0.1:6688',
|
||||||
'uri' => '/runtime', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问
|
'uri' => '/runtime', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问
|
||||||
'algo' => 'sha1',
|
'algo' => 'sha1',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ global $argv;
|
|||||||
return [
|
return [
|
||||||
'webman' => [
|
'webman' => [
|
||||||
'handler' => Http::class,
|
'handler' => Http::class,
|
||||||
'listen' => 'http://0.0.0.0:8787',
|
'listen' => 'http://0.0.0.0:6688',
|
||||||
'count' => cpu_count() * 4,
|
'count' => cpu_count() * 4,
|
||||||
'user' => '',
|
'user' => '',
|
||||||
'group' => '',
|
'group' => '',
|
||||||
|
|||||||
@@ -13,9 +13,29 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use Webman\Route;
|
use Webman\Route;
|
||||||
|
use app\api\middleware\CheckAuthTokenMiddleware;
|
||||||
|
use app\api\middleware\CheckUserTokenMiddleware;
|
||||||
|
|
||||||
|
// 仅需 auth-token 的路由组(authToken 接口在中间件内白名单跳过)
|
||||||
|
Route::group('/api', function () {
|
||||||
|
Route::any('/authToken', [app\api\controller\AuthTokenController::class, 'index']);
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
// 默认缓存驱动
|
// 默认缓存驱动(API 用户缓存依赖 Redis,建议设为 redis 并配置 REDIS_*)
|
||||||
'default' => env('CACHE_MODE', 'file'),
|
'default' => env('CACHE_MODE', 'redis'),
|
||||||
// 缓存连接方式配置
|
// 缓存连接方式配置
|
||||||
'stores' => [
|
'stores' => [
|
||||||
// redis缓存
|
// redis缓存
|
||||||
|
|||||||
@@ -49,9 +49,12 @@ class LoginController extends BaseController
|
|||||||
|
|
||||||
$code = $request->post('code', '');
|
$code = $request->post('code', '');
|
||||||
$uuid = $request->post('uuid', '');
|
$uuid = $request->post('uuid', '');
|
||||||
$captcha = new Captcha();
|
$captchaEnabled = config('plugin.saiadmin.saithink.captcha.enable', true);
|
||||||
if (!$captcha->checkCaptcha($uuid, $code)) {
|
if ($captchaEnabled) {
|
||||||
return $this->fail('验证码错误');
|
$captcha = new Captcha();
|
||||||
|
if (!$captcha->checkCaptcha($uuid, $code)) {
|
||||||
|
return $this->fail('验证码错误');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$logic = new SystemUserLogic();
|
$logic = new SystemUserLogic();
|
||||||
$data = $logic->login($username, $password, $type);
|
$data = $logic->login($username, $password, $type);
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ class BaseController extends OpenController
|
|||||||
protected $adminInfo;
|
protected $adminInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前登陆管理员ID
|
* 当前登陆管理员ID(未登录时为 null)
|
||||||
*/
|
*/
|
||||||
protected int $adminId;
|
protected ?int $adminId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前登陆管理员账号
|
* 当前登陆管理员账号(未登录时为空字符串)
|
||||||
*/
|
*/
|
||||||
protected string $adminName;
|
protected string $adminName = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 逻辑层注入
|
* 逻辑层注入
|
||||||
|
|||||||
@@ -42,11 +42,12 @@ class OpenController
|
|||||||
/**
|
/**
|
||||||
* 失败返回json内容
|
* 失败返回json内容
|
||||||
* @param string $msg
|
* @param string $msg
|
||||||
|
* @param int $code 201=请携带token 202=缺少参数 203=token过期,默认400
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function fail(string $msg = 'fail'): Response
|
public function fail(string $msg = 'fail', int $code = 400): Response
|
||||||
{
|
{
|
||||||
return json(['code' => 400, 'message' => $msg]);
|
return json(['code' => $code, 'message' => $msg]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return [
|
|||||||
|
|
||||||
'access_exp' => 8 * 60 * 60, // 登录token有效期,默认8小时
|
'access_exp' => 8 * 60 * 60, // 登录token有效期,默认8小时
|
||||||
|
|
||||||
// 验证码存储模式
|
// 验证码配置
|
||||||
'captcha' => [
|
'captcha' => [
|
||||||
// 验证码存储模式 session或者cache
|
// 是否启用登录验证码。改为 false 即关闭;也可用环境变量 LOGIN_CAPTCHA_ENABLE=0 关闭
|
||||||
|
'enable' => filter_var(getenv('LOGIN_CAPTCHA_ENABLE') ?: '0', FILTER_VALIDATE_BOOLEAN),
|
||||||
|
// 验证码存储模式 session 或 cache
|
||||||
'mode' => getenv('CAPTCHA_MODE'),
|
'mode' => getenv('CAPTCHA_MODE'),
|
||||||
// 验证码过期时间 (秒)
|
// 验证码过期时间 (秒)
|
||||||
'expire' => 300,
|
'expire' => 300,
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ namespace support;
|
|||||||
*/
|
*/
|
||||||
class Request extends \Webman\Http\Request
|
class Request extends \Webman\Http\Request
|
||||||
{
|
{
|
||||||
|
/** 由 CheckUserTokenMiddleware 注入:当前用户 ID */
|
||||||
|
public ?int $user_id = null;
|
||||||
|
|
||||||
|
/** 由 CheckUserTokenMiddleware 注入:当前 user-token 原始字符串 */
|
||||||
|
public ?string $userToken = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取参数增强方法
|
* 获取参数增强方法
|
||||||
|
|||||||
Reference in New Issue
Block a user