1.优化开奖逻辑

2.优化后台开奖派彩
3.优化接口规范
This commit is contained in:
2026-04-17 13:56:13 +08:00
parent 3cf386756b
commit bf3d50a309
50 changed files with 1036 additions and 770 deletions

View File

@@ -25,9 +25,9 @@ class Channel extends Backend
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'user_count', 'profit_amount', 'create_time', 'update_time'];
protected array|string $preExcludeFields = ['id', 'user_count', 'profit_amount', 'create_time', 'update_time', 'admin_id'];
protected array $withJoinTable = ['adminGroup', 'admin'];
protected array $withJoinTable = [];
protected string|array $quickSearchField = ['id', 'code', 'name'];
@@ -51,7 +51,7 @@ class Channel extends Backend
if ($response !== null) return $response;
$query = Db::name('channel')
->field(['id', 'name', 'admin_group_id'])
->field(['id', 'name'])
->order('id', 'asc');
if (!$this->auth->isSuperAdmin()) {
$query = $query->where('id', 'in', $this->currentChannelIds ?: [0]);
@@ -79,11 +79,16 @@ class Channel extends Backend
$tree = [];
foreach ($channels as $ch) {
$groupId = $ch['admin_group_id'] ?? null;
$channelId = (int) ($ch['id'] ?? 0);
$rootGroupIds = Db::name('admin_group')
->where('channel_id', $channelId)
->where('pid', 0)
->where('status', 1)
->column('id');
$groupIds = [];
if ($groupId !== null && $groupId !== '') {
$groupIds[] = $groupId;
foreach ($getGroupChildren($groupId) as $gid) {
foreach ($rootGroupIds as $rootId) {
$groupIds[] = $rootId;
foreach ($getGroupChildren($rootId) as $gid) {
$groupIds[] = $gid;
}
}
@@ -130,7 +135,7 @@ class Channel extends Backend
}
/**
* 添加(重写:管理员只选顶级组;admin_group_id 后端自动写入
* 添加(重写:渠道与角色组在「角色组」侧绑定 channel_id此处不再写入 admin_group_id
* @throws Throwable
*/
protected function _add(): Response
@@ -150,30 +155,10 @@ class Channel extends Backend
}
unset($data['invite_code']);
$adminId = $data['admin_id'] ?? null;
if ($adminId === null || $adminId === '') {
return $this->error(__('Parameter %s can not be empty', ['admin_id']));
}
if (array_key_exists('admin_group_id', $data)) {
unset($data['admin_group_id']);
}
$topGroupId = Db::name('admin_group_access')
->alias('aga')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('aga.uid', $adminId)
->where('ag.pid', 0)
->value('ag.id');
if ($topGroupId === null || $topGroupId === '') {
return $this->error(__('Record not found'));
}
$data['admin_group_id'] = $topGroupId;
if (!$this->auth->isSuperAdmin()) {
$data['admin_id'] = $this->auth->id;
}
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$data[$this->dataLimitField] = $this->auth->id;
}
@@ -207,7 +192,7 @@ class Channel extends Backend
}
/**
* 编辑(重写:管理员只选顶级组;admin_group_id 后端自动写入
* 编辑(重写:不再维护 channel.admin_group_id
* @throws Throwable
*/
protected function _edit(): Response
@@ -246,24 +231,6 @@ class Channel extends Backend
unset($data['admin_group_id']);
}
$nextAdminId = array_key_exists('admin_id', $data) ? $data['admin_id'] : ($row['admin_id'] ?? null);
if ($nextAdminId !== null && $nextAdminId !== '') {
$topGroupId = Db::name('admin_group_access')
->alias('aga')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('aga.uid', $nextAdminId)
->where('ag.pid', 0)
->value('ag.id');
if ($topGroupId === null || $topGroupId === '') {
return $this->error(__('Record not found'));
}
$data['admin_group_id'] = $topGroupId;
}
if (!$this->auth->isSuperAdmin()) {
$data['admin_id'] = $this->auth->id;
}
$result = false;
$this->model->startTrans();
try {
@@ -310,9 +277,6 @@ class Channel extends Backend
$where[] = [$alias['channel'] . '.id', 'in', $this->currentChannelIds ?: [0]];
}
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible(['adminGroup' => ['name'], 'admin' => ['username']])
->alias($alias)
->where($where)
->order($order)
@@ -392,9 +356,9 @@ class Channel extends Backend
return $this->error('结算单号已存在,请稍后重试');
}
$adminId = $row['admin_id'] ?? null;
if ($adminId === null || $adminId === '' || (int) $adminId <= 0) {
return $this->error('渠道未绑定代理管理员,无法生成佣金记录');
$adminId = $this->resolveCommissionAdminIdForChannel((int) $row['id']);
if ($adminId === null || $adminId <= 0) {
return $this->error('渠道下无归属管理员账号(请为管理员设置所属渠道),无法生成佣金记录');
}
$now = time();
@@ -685,8 +649,25 @@ class Channel extends Backend
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$byAdmin = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $byAdmin)));
return array_values(array_unique($ids));
}
/**
* 佣金归属管理员:取该渠道下 admin.channel_id 匹配的首个管理员(按 id 升序)。
*/
private function resolveCommissionAdminIdForChannel(int $channelId): ?int
{
if ($channelId <= 0) {
return null;
}
$aid = Db::name('admin')
->where('channel_id', $channelId)
->order('id', 'asc')
->value('id');
if ($aid === null || $aid === '') {
return null;
}
return (int) $aid;
}
private function normalizeAgentModeFields(array $data): array

View File

@@ -187,12 +187,21 @@ class Admin extends Backend
$passwd = $data['password'] ?? '';
$data = $this->excludeFields($data);
$creatorChannelId = $this->getCreatorChannelId();
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []);
if (!$this->auth->isSuperAdmin()) {
if ($creatorChannelId === null || $creatorChannelId === '') {
return $this->error(__('You have no permission'));
}
if ($groupChannelId === null || $groupChannelId === '') {
return $this->error('所选角色组未绑定渠道');
}
if ((string) $groupChannelId !== (string) $creatorChannelId) {
return $this->error('所选角色组渠道与当前账号不一致');
}
$data['channel_id'] = $creatorChannelId;
$data['parent_admin_id'] = $this->auth->id;
} else {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
}
$data['invite_code'] = $this->generateUniqueInviteCode();
$requireCommissionRate = $this->requireCommissionRate($data['group_arr'] ?? []);
@@ -343,8 +352,20 @@ class Admin extends Backend
$data = $this->excludeFields($data);
unset($data['invite_code']);
$creatorChannelId = $this->getCreatorChannelId();
if (!$this->auth->isSuperAdmin() && $creatorChannelId !== null && $creatorChannelId !== '') {
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []);
if (!$this->auth->isSuperAdmin()) {
if ($creatorChannelId === null || $creatorChannelId === '') {
return $this->error(__('You have no permission'));
}
if ($groupChannelId === null || $groupChannelId === '') {
return $this->error('所选角色组未绑定渠道');
}
if ((string) $groupChannelId !== (string) $creatorChannelId) {
return $this->error('所选角色组渠道与当前账号不一致');
}
$data['channel_id'] = $creatorChannelId;
} else {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
}
$requireCommissionRate = $this->requireCommissionRate($data['group_arr'] ?? []);
if ($requireCommissionRate) {
@@ -463,10 +484,24 @@ class Admin extends Backend
if ($currentAdmin && !empty($currentAdmin['channel_id'])) {
return $currentAdmin['channel_id'];
}
$channelId = Db::name('channel')
->where('admin_id', $this->auth->id)
->value('id');
return $channelId ?: null;
return null;
}
/**
* @param array<int|string> $groupIds
*/
private function resolveChannelIdFromPrimaryGroup(array $groupIds): mixed
{
if ($groupIds === []) {
return null;
}
$gid = $groupIds[0];
if ($gid === null || $gid === '') {
return null;
}
return Db::name('admin_group')->where('id', $gid)->value('channel_id');
}
private function generateUniqueInviteCode(): string

View File

@@ -86,6 +86,10 @@ class Group extends Backend
if (!$this->auth->isSuperAdmin() && $pidInt !== 0 && !in_array($pidInt, $this->manageableGroupIds, true)) {
return $this->error(__('You have no permission'));
}
$inheritRes = $this->applyChannelInheritance($data, $pidInt);
if ($inheritRes !== null) {
return $inheritRes;
}
$shouldHandleCommissionRate = true;
if ($shouldHandleCommissionRate) {
if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) {
@@ -165,6 +169,10 @@ class Group extends Backend
if (!$this->auth->isSuperAdmin() && $pidInt !== 0 && !in_array($pidInt, $this->manageableGroupIds, true)) {
return $this->error(__('You have no permission'));
}
$inheritRes = $this->applyChannelInheritance($data, $pidInt);
if ($inheritRes !== null) {
return $inheritRes;
}
$shouldHandleCommissionRate = true;
if ($shouldHandleCommissionRate) {
if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) {
@@ -204,6 +212,7 @@ class Group extends Backend
return $this->error($e->getMessage());
}
if ($result !== false) {
$this->syncDescendantChannelIds(intval((string)$row['id']));
return $this->success(__('Update successful'));
}
return $this->error(__('No rows updated'));
@@ -223,11 +232,39 @@ class Group extends Backend
}
$rowData = $row->toArray();
$rowData['rules'] = array_values($rules);
$rowData = $this->enrichChannelDisplay($rowData);
return $this->success('', [
'row' => $rowData
]);
}
/**
* 表单只读展示:根据 channel_id 解析渠道名称与渠道负责人admin.channel_id → admin.username取首个
*/
public function channelBindPreview(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$cid = $request->get('channel_id') ?? $request->post('channel_id');
if ($cid === null || $cid === '') {
return $this->success('', [
'channel_name' => '',
'channel_admin_username' => '',
]);
}
if (!Db::name('channel')->where('id', $cid)->value('id')) {
return $this->error(__('Record not found'));
}
$row = $this->enrichChannelDisplay(['channel_id' => $cid]);
return $this->success('', [
'channel_name' => $row['channel_name'] ?? '',
'channel_admin_username' => $row['channel_admin_username'] ?? '',
]);
}
public function del(Request $request): Response
{
$response = $this->initializeBackend($request);
@@ -353,7 +390,21 @@ class Group extends Backend
}
$data = $this->model->where($where)->select()->toArray();
$channelIds = [];
foreach ($data as $datum) {
$c = $datum['channel_id'] ?? null;
if ($c !== null && $c !== '') {
$channelIds[] = $c;
}
}
$channelNames = [];
if ($channelIds !== []) {
$channelNames = Db::name('channel')->where('id', 'in', array_unique($channelIds))->column('name', 'id');
}
foreach ($data as &$datum) {
$c = $datum['channel_id'] ?? null;
$datum['channel_name'] = ($c !== null && $c !== '') ? ($channelNames[$c] ?? '') : '';
if ($datum['rules']) {
if ($datum['rules'] == '*') {
$datum['rules'] = __('Super administrator');
@@ -368,6 +419,7 @@ class Group extends Backend
$datum['rules'] = __('No permission');
}
}
unset($datum);
return $this->assembleTree ? $this->tree->assembleChild($data) : $data;
}
@@ -418,4 +470,85 @@ class Group extends Backend
return null;
}
/**
* 顶级角色组可选渠道;子级继承父级 channel_id不信任客户端提交的子级 channel_id
*
* @param array<string, mixed> $data
*/
private function applyChannelInheritance(array &$data, int $pidInt): ?Response
{
if ($pidInt === 0) {
if (!$this->auth->isSuperAdmin()) {
unset($data['channel_id']);
$cc = $this->getCreatorChannelId();
if ($cc !== null && $cc !== '') {
$data['channel_id'] = $cc;
}
}
$cid = $data['channel_id'] ?? null;
if ($cid !== null && $cid !== '') {
$exists = Db::name('channel')->where('id', $cid)->value('id');
if (!$exists) {
return $this->error(__('Record not found'));
}
}
return null;
}
unset($data['channel_id']);
$parent = Db::name('admin_group')->where('id', $pidInt)->find();
if (!$parent) {
return $this->error(__('Record not found'));
}
$data['channel_id'] = $parent['channel_id'];
return null;
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private function enrichChannelDisplay(array $row): array
{
$row['channel_name'] = '';
$row['channel_admin_username'] = '';
$cid = $row['channel_id'] ?? null;
if ($cid === null || $cid === '') {
return $row;
}
$ch = Db::name('channel')->where('id', $cid)->field(['id', 'name'])->find();
if (!$ch) {
return $row;
}
$row['channel_name'] = $ch['name'] ?? '';
$row['channel_admin_username'] = (string) (Db::name('admin')->where('channel_id', $cid)->order('id', 'asc')->value('username') ?? '');
return $row;
}
private function syncDescendantChannelIds(int $groupId): void
{
$channelId = Db::name('admin_group')->where('id', $groupId)->value('channel_id');
$children = Db::name('admin_group')->where('pid', $groupId)->column('id');
foreach ($children as $childId) {
Db::name('admin_group')->where('id', $childId)->update(['channel_id' => $channelId]);
$this->syncDescendantChannelIds($childId);
}
}
private function getCreatorChannelId(): mixed
{
$currentAdmin = Db::name('admin')
->field(['id', 'channel_id'])
->where('id', $this->auth->id)
->find();
if ($currentAdmin && !empty($currentAdmin['channel_id'])) {
return $currentAdmin['channel_id'];
}
return null;
}
}

View File

@@ -13,7 +13,7 @@ class Record extends Backend
{
protected ?object $model = null;
protected string|array $preExcludeFields = ['id', 'create_time', 'update_time'];
protected string|array $preExcludeFields = ['id', 'create_time', 'update_time', 'platform_profit_amount', 'winner_user_count'];
protected string|array $quickSearchField = ['id', 'period_no'];

View File

@@ -122,7 +122,6 @@ class BetOrder extends Backend
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$byAdmin = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $byAdmin)));
return array_values(array_unique($ids));
}
}

View File

@@ -81,7 +81,6 @@ class DepositOrder extends Backend
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$byAdmin = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $byAdmin)));
return array_values(array_unique($ids));
}
}

View File

@@ -82,7 +82,6 @@ class WithdrawOrder extends Backend
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$byAdmin = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $byAdmin)));
return array_values(array_unique($ids));
}
}

View File

@@ -20,7 +20,7 @@ class User extends Backend
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'uuid', 'create_time', 'update_time'];
protected array|string $preExcludeFields = ['id', 'uuid', 'create_time', 'update_time', 'invite_code'];
protected array $withJoinTable = ['channel', 'admin'];
@@ -35,7 +35,7 @@ class User extends Backend
}
/**
* 添加重写password 使用 Admin 同款加密uuid 由 username+channel_id 生成
* 添加重写password 使用 Admin 同款加密uuid 为 10 位唯一对外标识
* @throws Throwable
*/
protected function _add(): Response
@@ -47,6 +47,10 @@ class User extends Backend
}
$data = $this->applyInputFilter($data);
$inviteResolved = $this->applyInviteCodeToUserChannel($data);
if ($inviteResolved !== null) {
return $inviteResolved;
}
$data = $this->excludeFields($data);
$password = $data['password'] ?? null;
@@ -60,7 +64,7 @@ class User extends Backend
if (!is_string($username) || trim($username) === '' || $channelId === null || $channelId === '') {
return $this->error(__('Parameter %s can not be empty', ['username/channel_id']));
}
$data['uuid'] = md5(trim($username) . '|' . $channelId);
$data['uuid'] = \app\common\model\User::generateUniquePublicCode10();
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$data[$this->dataLimitField] = $this->auth->id;
@@ -95,7 +99,7 @@ class User extends Backend
}
/**
* 编辑重写password 使用 Admin 同款加密uuid 由 username+channel_id 生成
* 编辑重写password 使用 Admin 同款加密uuid 创建后不因改名改渠道自动变更
* @throws Throwable
*/
protected function _edit(): Response
@@ -130,18 +134,6 @@ class User extends Backend
}
}
$nextUsername = array_key_exists('username', $data) ? $data['username'] : $row['username'];
$nextChannelId = null;
if (array_key_exists('channel_id', $data)) {
$nextChannelId = $data['channel_id'];
} else {
$nextChannelId = $row['channel_id'] ?? null;
}
if (is_string($nextUsername) && trim($nextUsername) !== '' && $nextChannelId !== null && $nextChannelId !== '') {
$data['uuid'] = md5(trim($nextUsername) . '|' . $nextChannelId);
}
$result = false;
$this->model->startTrans();
try {
@@ -337,6 +329,63 @@ class User extends Backend
return array_values(array_unique(array_merge($own, $children)));
}
/**
* 请求中的 `invite_code`(子代理 admin.invite_code解析为 `channel_id`,并写入 `register_invite_code`、`admin_id`。
* 若同时提交 `channel_id` / `admin_id`,须与邀请码对应记录一致。
*/
private function applyInviteCodeToUserChannel(array &$data): ?Response
{
if (!array_key_exists('invite_code', $data)) {
return null;
}
$raw = $data['invite_code'];
unset($data['invite_code']);
$inviteCode = is_string($raw) ? trim($raw) : '';
if ($inviteCode === '') {
return null;
}
$row = Db::name('admin')->field(['id', 'channel_id'])->where('invite_code', $inviteCode)->find();
if (!$row) {
return $this->error(__('Invite code does not exist'));
}
$cid = $row['channel_id'] ?? null;
if ($cid === null || $cid === '') {
return $this->error(__('Invite code not bound to channel'));
}
$cidInt = intval(trim((string) $cid));
if ($cidInt <= 0) {
return $this->error(__('Invite code not bound to channel'));
}
if (isset($data['channel_id']) && $data['channel_id'] !== null && $data['channel_id'] !== '') {
$existing = intval(trim((string) $data['channel_id']));
if ($existing > 0 && $existing !== $cidInt) {
return $this->error(__('Parameter error'));
}
}
$data['channel_id'] = $cidInt;
$data['register_invite_code'] = $inviteCode;
$aidRaw = $row['id'] ?? null;
if ($aidRaw === null || $aidRaw === '') {
return $this->error(__('Parameter error'));
}
$aidInt = intval(trim((string) $aidRaw));
if ($aidInt <= 0) {
return $this->error(__('Parameter error'));
}
if (isset($data['admin_id']) && $data['admin_id'] !== null && $data['admin_id'] !== '') {
$reqAid = intval(trim((string) $data['admin_id']));
if ($reqAid > 0 && $reqAid !== $aidInt) {
return $this->error(__('Parameter error'));
}
}
$data['admin_id'] = $aidInt;
return null;
}
/**
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
*/

View File

@@ -1,6 +1,6 @@
<?php
namespace app\admin\controller\record;
namespace app\admin\controller\user;
use app\common\controller\Backend;
use support\think\Db;
@@ -124,7 +124,6 @@ class UserWalletRecord extends Backend
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$byAdmin = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $byAdmin)));
return array_values(array_unique($ids));
}
}

View File

@@ -45,6 +45,7 @@ class Account extends Frontend
'code' => 1,
'message' => __('ok'),
'data' => [
'uuid' => $user->uuid ?? '',
'username' => $user->username,
'head_image' => $user->avatar ?? '',
'coin' => $user->coin,

View File

@@ -34,6 +34,9 @@ class Auth extends MobileBase
if ($username === '' || $password === '') {
return $this->mobileError(1001, 'Missing parameters');
}
if ($inviteCode === '') {
return $this->mobileError(1001, 'Invite code required');
}
if (!preg_match('/^1[3-9]\d{9}$/', $username)) {
return $this->mobileError(1003, 'Please enter the correct mobile number');
}
@@ -41,19 +44,33 @@ class Auth extends MobileBase
$phone = $username;
$email = '';
$extend = [];
if ($inviteCode !== '') {
$inviterAdmin = Db::name('admin')->field(['id', 'channel_id'])->where('invite_code', $inviteCode)->find();
if (!$inviterAdmin) {
return $this->mobileError(2002, 'Invite code does not exist');
}
$extend['register_invite_code'] = $inviteCode;
$extend['admin_id'] = $inviterAdmin['id'];
$extend['channel_id'] = $inviterAdmin['channel_id'] ?? null;
if (User::where('username', $username)->find() || User::where('phone', $username)->find()) {
return $this->mobileError(2003, 'Account already registered', [
'already_registered' => true,
]);
}
$extend = [];
$inviterAdmin = Db::name('admin')->field(['id', 'channel_id'])->where('invite_code', $inviteCode)->find();
if (!$inviterAdmin) {
return $this->mobileError(2002, 'Invite code does not exist');
}
$extend['register_invite_code'] = $inviteCode;
$extend['admin_id'] = $inviterAdmin['id'];
$channelId = $inviterAdmin['channel_id'] ?? null;
if ($channelId === null || $channelId === '' || (int) $channelId <= 0) {
return $this->mobileError(2002, 'Invite code not bound to channel');
}
$extend['channel_id'] = (int) $channelId;
$registered = $this->auth->register($username, $password, $phone, $email, 1, $extend);
if (!$registered) {
$dup = $this->auth->getRegisterDuplicateKind();
if ($dup === 'username' || $dup === 'email' || $dup === 'phone') {
return $this->mobileError(2003, 'Account already registered', [
'already_registered' => true,
]);
}
return $this->mobileError(2000, (string) $this->auth->getError());
}
@@ -122,6 +139,7 @@ class Auth extends MobileBase
'expires_in' => config('buildadmin.user_token_keep_time', 259200),
'user' => [
'username' => $userInfo['username'] ?? '',
'uuid' => $userInfo['uuid'] ?? '',
'coin' => $userInfo['coin'] ?? '0.0000',
'channel_id' => $userInfo['channel_id'] ?? null,
'risk_flags' => $userInfo['risk_flags'] ?? 0,

View File

@@ -54,7 +54,7 @@ class Game extends MobileBase
'open_at' => $openAt,
],
'bet_config' => [
'max_select_count' => $this->intValue($this->getConfigValue('max_select_count', '5')),
'pick_max_number_count' => $this->getPickMaxNumberCount(),
'chips' => ['1.0000', '5.0000', '10.0000', '25.0000', '50.0000', '100.0000'],
'single_number_max_bet' => $this->getConfigValue('single_number_max_bet', '500.0000'),
],
@@ -140,13 +140,18 @@ class Game extends MobileBase
return $response;
}
$periodNo = trim((string) $request->post('period_no', ''));
$numbers = $request->post('numbers', []);
$numbersRaw = $request->post('numbers', '');
$betAmount = (string) $request->post('bet_amount', '');
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
if ($periodNo === '' || !is_array($numbers) || $betAmount === '' || $idempotencyKey === '') {
if ($periodNo === '' || $betAmount === '' || $idempotencyKey === '') {
return $this->mobileError(1001, 'Missing parameters');
}
if (count($numbers) < 1) {
$numbers = $this->parseBetNumbersFromRequest($numbersRaw);
if ($numbers === []) {
return $this->mobileError(1003, 'Invalid parameter value');
}
$maxSelect = $this->getPickMaxNumberCount();
if (count($numbers) > $maxSelect) {
return $this->mobileError(1003, 'Invalid parameter value');
}
@@ -285,6 +290,49 @@ class Game extends MobileBase
]);
}
/**
* 下注号码:`numbers` 为逗号分隔字符串(如 `1,8,16`);兼容旧版 JSON 数组。
*
* @return list<int>
*/
private function parseBetNumbersFromRequest($numbersRaw): array
{
if (is_array($numbersRaw)) {
$out = [];
foreach ($numbersRaw as $v) {
$n = filter_var($v, FILTER_VALIDATE_INT);
if ($n === false || $n < 1 || $n > 36) {
return [];
}
$out[] = $n;
}
$out = array_values(array_unique($out));
sort($out);
return $out;
}
$raw = trim((string) $numbersRaw);
if ($raw === '') {
return [];
}
$parts = preg_split('/\s*,\s*/', $raw);
$out = [];
foreach ($parts as $p) {
if ($p === '') {
continue;
}
$n = filter_var($p, FILTER_VALIDATE_INT);
if ($n === false || $n < 1 || $n > 36) {
return [];
}
$out[] = $n;
}
$out = array_values(array_unique($out));
sort($out);
return $out;
}
private function mapPeriodStatus($status): string
{
if ($this->intValue($status) === 0) {
@@ -299,6 +347,22 @@ class Game extends MobileBase
return 'finished';
}
/**
* 单注最多可选号码个数:`game_config.config_key = pick_max_number_count`
*/
private function getPickMaxNumberCount(): int
{
$v = $this->intValue($this->getConfigValue('pick_max_number_count', '10'));
if ($v < 1) {
return 1;
}
if ($v > 36) {
return 36;
}
return $v;
}
private function getConfigValue(string $key, string $default): string
{
$value = GameConfig::where('config_key', $key)->value('config_value');

View File

@@ -81,11 +81,15 @@ class User extends Frontend
->where('invite_code', $params['invite_code'])
->find();
if (!$inviterAdmin) {
return $this->error(__('Parameter error'));
return $this->error(__('Invite code does not exist'));
}
$ch = $inviterAdmin['channel_id'] ?? null;
if ($ch === null || $ch === '' || intval(trim((string) $ch)) <= 0) {
return $this->error(__('Invite code not bound to channel'));
}
$extend['register_invite_code'] = $params['invite_code'];
$extend['inviter_admin_id'] = $inviterAdmin['id'];
$extend['channel_id'] = $inviterAdmin['channel_id'] ?? null;
$extend['admin_id'] = $inviterAdmin['id'];
$extend['channel_id'] = intval(trim((string) $ch));
}
$res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email'], 1, $extend);
}
@@ -96,6 +100,10 @@ class User extends Frontend
'routePath' => '/user'
]);
}
$dup = $this->auth->getRegisterDuplicateKind();
if ($params['tab'] === 'register' && ($dup === 'username' || $dup === 'email' || $dup === 'phone')) {
return $this->error(__('Account already registered'));
}
$msg = $this->auth->getError();
return $this->error($msg ?: __('Check in failed, please try again or contact the website administrator~'));
}

View File

@@ -23,6 +23,9 @@ return [
'Invalid timestamp' => 'Invalid timestamp',
'Invite code does not exist' => 'Invite code does not exist',
'Register only supports phone' => 'Register only supports phone',
'Invite code required' => 'Invite code is required',
'Invite code not bound to channel' => 'This invite code is not bound to a valid channel',
'Account already registered' => 'This phone number is already registered. Please sign in.',
'Please enter the correct mobile number' => 'Please enter the correct mobile number',
'Registered successfully but login failed' => 'Registered successfully but login failed',
'Incorrect account or password' => 'Incorrect account or password',

View File

@@ -55,6 +55,9 @@ return [
'Invalid timestamp' => '时间戳无效',
'Invite code does not exist' => '邀请码不存在',
'Register only supports phone' => '注册仅支持手机号',
'Invite code required' => '请填写邀请码',
'Invite code not bound to channel' => '该邀请码未绑定有效渠道',
'Account already registered' => '该手机号已注册,请直接登录',
'Please enter the correct mobile number' => '请输入正确的手机号',
'Registered successfully but login failed' => '注册成功但登录失败',
'Incorrect account or password' => '账号或密码错误',

View File

@@ -27,7 +27,14 @@ class Auth extends \ba\Auth
protected int $keepTime = 86400;
protected int $refreshTokenKeepTime = 2592000;
protected array $allowFields = ['id', 'username', 'nickname', 'email', 'mobile', 'avatar', 'gender', 'birthday', 'money', 'score', 'join_time', 'motto', 'last_login_time', 'last_login_ip'];
/** 注册失败原因:`username`/`email` 表示账号已占用,供接口返回友好提示 */
protected string $registerDuplicateKind = '';
protected array $allowFields = [
'id', 'username', 'nickname', 'email', 'phone', 'avatar', 'gender', 'birthday',
'coin', 'channel_id', 'risk_flags', 'uuid',
'join_time', 'motto', 'last_login_time', 'last_login_ip',
];
public function __construct(array $config = [])
{
@@ -73,7 +80,7 @@ class Auth extends \ba\Auth
return false;
}
$this->token = $token;
$this->loginSuccessful();
$this->loginEd = true;
return true;
}
}
@@ -84,6 +91,7 @@ class Auth extends \ba\Auth
public function register(string $username, string $password = '', string $phone = '', string $email = '', int $group = 1, array $extend = []): bool
{
$this->registerDuplicateKind = '';
$request = function_exists('request') ? request() : null;
$ip = $request ? $request->getRealIp() : '0.0.0.0';
@@ -98,13 +106,20 @@ class Auth extends \ba\Auth
return false;
}
if (User::where('email', $email)->find() && $email) {
$this->registerDuplicateKind = 'email';
$this->setError(__('Email') . ' ' . __('already exists'));
return false;
}
if (User::where('username', $username)->find()) {
$this->registerDuplicateKind = 'username';
$this->setError(__('Username') . ' ' . __('already exists'));
return false;
}
if ($phone !== '' && User::where('phone', $phone)->find()) {
$this->registerDuplicateKind = 'phone';
$this->setError(__('Mobile') . ' ' . __('already exists'));
return false;
}
$nickname = preg_replace_callback('/1[3-9]\d{9}/', fn($m) => substr($m[0], 0, 3) . '****' . substr($m[0], 7), $username);
$time = time();
@@ -116,6 +131,8 @@ class Auth extends \ba\Auth
'last_login_ip' => $ip,
'last_login_time' => $time,
'status' => 1,
'uuid' => User::generateUniquePublicCode10(),
'remark' => User::formatLoginRemark($time, $ip),
];
$data = array_merge(compact('username', 'password', 'phone', 'email'), $data, $extend);
@@ -215,6 +232,7 @@ class Auth extends \ba\Auth
$this->model->login_failure = 0;
$this->model->last_login_time = time();
$this->model->last_login_ip = $ip;
$this->model->remark = User::formatLoginRemark(time(), $ip);
$this->model->save();
$this->loginEd = true;
$this->model->commit();
@@ -344,6 +362,11 @@ class Auth extends \ba\Auth
return $this->error ? __($this->error) : '';
}
public function getRegisterDuplicateKind(): string
{
return $this->registerDuplicateKind;
}
protected function reset(bool $deleteToken = true): bool
{
if ($deleteToken && $this->token) {
@@ -353,6 +376,7 @@ class Auth extends \ba\Auth
$this->loginEd = false;
$this->model = null;
$this->refreshToken = '';
$this->registerDuplicateKind = '';
$this->setError('');
$this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
return true;

View File

@@ -29,14 +29,4 @@ class Channel extends Model
{
return is_null($value) ? null : (float)$value;
}
public function adminGroup(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\AdminGroup::class, 'admin_group_id', 'id');
}
public function admin(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
}
}

View File

@@ -11,13 +11,15 @@ class GameRecord extends Model
protected $autoWriteTimestamp = true;
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'period_start_at' => 'integer',
'status' => 'integer',
'draw_mode' => 'integer',
'preset_number' => 'integer',
'result_number' => 'integer',
'create_time' => 'integer',
'update_time' => 'integer',
'period_start_at' => 'integer',
'status' => 'integer',
'draw_mode' => 'integer',
'preset_number' => 'integer',
'result_number' => 'integer',
'platform_profit_amount' => 'string',
'winner_user_count' => 'integer',
];
public function setPeriodStartAtAttr($value, $data = [])

View File

@@ -13,6 +13,30 @@ class User extends Model
protected $autoWriteTimestamp = true;
/**
* 生成 10 位唯一对外标识(大写字母与数字,排除易混淆字符)
*/
public static function generateUniquePublicCode10(): string
{
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$len = strlen($chars);
for ($attempt = 0; $attempt < 80; $attempt++) {
$code = '';
for ($i = 0; $i < 10; $i++) {
$code .= $chars[random_int(0, $len - 1)];
}
if (!self::where('uuid', $code)->find()) {
return $code;
}
}
throw new \RuntimeException('Failed to generate unique user uuid');
}
public static function formatLoginRemark(int $timestamp, string $ip): string
{
return '最后登录:' . date('Y-m-d H:i:s', $timestamp) . ' IP:' . $ip;
}
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
use Throwable;
/**
* 开奖后结算注单:写入 win_amount、status=已结算;中奖时入账并记 user_wallet_recordbiz_type=payout
*/
final class GameBetSettleService
{
private const BASE_ODDS = 33;
/**
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
*
* @throws Throwable
*/
public static function settleBetsForDraw(int $recordId, int $resultNumber): void
{
if ($recordId <= 0 || $resultNumber < 1) {
return;
}
$now = time();
$bets = Db::name('bet_order')
->where('period_id', $recordId)
->where('status', 1)
->order('id', 'asc')
->select()
->toArray();
foreach ($bets as $bet) {
$betId = (int) ($bet['id'] ?? 0);
if ($betId <= 0) {
continue;
}
$win = self::computeWinAmount($bet, $resultNumber);
$jackpot = '0.0000';
$affected = Db::name('bet_order')
->where('id', $betId)
->where('status', 1)
->update([
'win_amount' => $win,
'jackpot_extra_amount' => $jackpot,
'status' => 2,
'update_time' => $now,
]);
if ($affected === 0) {
continue;
}
if (bccomp($win, '0', 4) <= 0) {
continue;
}
self::creditUserPayout($bet, $betId, $win, $now);
}
}
/**
* 补偿:库中已结束局次但注单仍为待开奖的,可重复调用(幂等)。
*/
public static function settlePendingForEndedRecords(): int
{
$rows = Db::name('game_record')
->where('status', 4)
->whereNotNull('result_number')
->field(['id', 'result_number'])
->order('id', 'asc')
->select()
->toArray();
$count = 0;
foreach ($rows as $row) {
$rid = (int) ($row['id'] ?? 0);
$rn = (int) ($row['result_number'] ?? 0);
if ($rid <= 0 || $rn < 1) {
continue;
}
$pending = Db::name('bet_order')
->where('period_id', $rid)
->where('status', 1)
->count();
if ($pending === 0) {
continue;
}
Db::startTrans();
try {
self::settleBetsForDraw($rid, $rn);
Db::commit();
$count++;
} catch (Throwable $e) {
Db::rollback();
throw $e;
}
}
return $count;
}
/**
* 单注应付派彩:命中开奖号码时 unit × (连胜+1) × 33与 GameLiveService 一致)。
*/
public static function computeWinAmount(array $bet, int $resultNumber): string
{
$pickNumbers = $bet['pick_numbers'] ?? null;
if (is_string($pickNumbers)) {
$decoded = json_decode($pickNumbers, true);
$pickNumbers = is_array($decoded) ? $decoded : [];
}
if (!is_array($pickNumbers)) {
$pickNumbers = [];
}
if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) {
return '0.0000';
}
$unit = (string) ($bet['unit_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
return bcmul($unit, $odds, 4);
}
private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now): void
{
$userId = (int) ($bet['user_id'] ?? 0);
if ($userId <= 0) {
return;
}
$idem = 'payout_bet_' . $betId;
if (Db::name('user_wallet_record')->where('idempotency_key', $idem)->value('id')) {
return;
}
$user = Db::name('user')->where('id', $userId)->find();
if (!$user) {
return;
}
$before = (string) ($user['coin'] ?? '0');
$after = bcadd($before, $winAmount, 4);
Db::name('user_wallet_record')->insert([
'user_id' => $userId,
'channel_id' => $bet['channel_id'] ?? null,
'biz_type' => 'payout',
'direction' => 1,
'amount' => $winAmount,
'balance_before' => $before,
'balance_after' => $after,
'ref_type' => 'bet_order',
'ref_id' => $betId,
'idempotency_key' => $idem,
'operator_admin_id' => null,
'remark' => '压注派彩',
'create_time' => $now,
]);
Db::name('user')->where('id', $userId)->update([
'coin' => $after,
'update_time' => $now,
]);
}
}

View File

@@ -17,6 +17,9 @@ final class GameLiveService
private const KEY_BET_SECONDS = 'bet_seconds';
private const KEY_PICK_MAX_NUMBER_COUNT = 'pick_max_number_count';
/** 开奖结果号码池1 至此上限(与单注可选号码个数配置无关) */
private const DRAW_NUMBER_MAX = 36;
public static function buildSnapshot(?int $recordId = null): array
{
$record = self::resolveRecord($recordId);
@@ -30,6 +33,7 @@ final class GameLiveService
'period_seconds' => self::getConfigInt(self::KEY_PERIOD_SECONDS, 30),
'bet_seconds' => self::getConfigInt(self::KEY_BET_SECONDS, 20),
'pick_max_number_count' => self::getPickMaxNumberCount(),
'draw_number_max' => self::DRAW_NUMBER_MAX,
'remaining_seconds' => 0,
'bet_remaining_seconds' => 0,
'can_calculate' => false,
@@ -59,7 +63,7 @@ final class GameLiveService
$status = (int) $record['status'];
$canCalculate = $elapsed >= $betSeconds && ($status === 0 || $status === 1);
if ($canCalculate) {
for ($n = 1; $n <= $pickMax; $n++) {
for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = [
'number' => $n,
@@ -97,6 +101,7 @@ final class GameLiveService
'period_seconds' => $periodSeconds,
'bet_seconds' => $betSeconds,
'pick_max_number_count' => $pickMax,
'draw_number_max' => self::DRAW_NUMBER_MAX,
'remaining_seconds' => $remaining,
'bet_remaining_seconds' => $betRemaining,
'can_calculate' => $canCalculate,
@@ -129,7 +134,7 @@ final class GameLiveService
}
$pickMax = self::getPickMaxNumberCount();
if ($manualNumber !== null && ($manualNumber < 1 || $manualNumber > $pickMax)) {
if ($manualNumber !== null && ($manualNumber < 1 || $manualNumber > self::DRAW_NUMBER_MAX)) {
return ['ok' => false, 'msg' => '手动开奖号码超出允许范围'];
}
@@ -138,7 +143,7 @@ final class GameLiveService
$bestNumber = null;
$bestLoss = null;
$bestNumbers = [];
for ($n = 1; $n <= $pickMax; $n++) {
for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = ['number' => $n, 'estimated_loss' => $loss];
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
@@ -165,6 +170,7 @@ final class GameLiveService
'period_seconds' => $periodSeconds,
'bet_seconds' => $betSeconds,
'pick_max_number_count' => $pickMax,
'draw_number_max' => self::DRAW_NUMBER_MAX,
'candidate_numbers' => $candidates,
'ai_default_number' => $bestNumber,
'final_number' => $finalNumber,
@@ -189,8 +195,10 @@ final class GameLiveService
'draw_mode' => $manualNumber === null ? 0 : 1,
'update_time' => $now,
]);
GameBetSettleService::settleBetsForDraw((int) $record['id'], $finalNumber);
GameRecordService::createNextRecordAfterDraw();
Db::commit();
GameRecordStatService::refreshForRecordId((int) $record['id']);
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => $e->getMessage()];

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
/**
* 对局维度统计:平台盈亏、中奖人数(与 GameLiveService 单号派彩口径一致)。
*/
final class GameRecordStatService
{
private const BASE_ODDS = 33;
/**
* 根据注单与开奖号码回写 game_record 统计字段(已结束对局)。
*/
public static function refreshForRecordId(int $recordId): void
{
if ($recordId <= 0) {
return;
}
$record = Db::name('game_record')->where('id', $recordId)->find();
if (!$record) {
return;
}
$status = (int) ($record['status'] ?? 0);
$now = time();
if ($status !== 4) {
Db::name('game_record')->where('id', $recordId)->update([
'platform_profit_amount' => '0.0000',
'winner_user_count' => 0,
'update_time' => $now,
]);
return;
}
$resultRaw = $record['result_number'] ?? null;
if ($resultRaw === null || $resultRaw === '') {
return;
}
$resultNum = (int) $resultRaw;
$bets = Db::name('bet_order')->where('period_id', $recordId)->select()->toArray();
$totalBet = '0.0000';
$totalPayout = '0.0000';
$winnerUserIds = [];
foreach ($bets as $bet) {
$st = (int) ($bet['status'] ?? 0);
if ($st === 3) {
continue;
}
$tb = (string) ($bet['total_amount'] ?? '0');
$totalBet = bcadd($totalBet, $tb, 4);
if ($st === 2) {
$payout = bcadd((string) ($bet['win_amount'] ?? '0'), (string) ($bet['jackpot_extra_amount'] ?? '0'), 4);
} else {
$payout = self::estimatePayoutForBet($bet, $resultNum);
}
$totalPayout = bcadd($totalPayout, $payout, 4);
if (bccomp($payout, '0', 4) > 0) {
$uid = (int) ($bet['user_id'] ?? 0);
if ($uid > 0) {
$winnerUserIds[$uid] = true;
}
}
}
$profit = bcsub($totalBet, $totalPayout, 4);
Db::name('game_record')->where('id', $recordId)->update([
'platform_profit_amount' => $profit,
'winner_user_count' => count($winnerUserIds),
'update_time' => $now,
]);
}
/**
* 与 GameLiveService::estimateLossForNumber 中单注派彩一致:命中号码时 unit × (streak+1) × 33。
*/
private static function estimatePayoutForBet(array $bet, int $resultNumber): string
{
$pickNumbers = $bet['pick_numbers'] ?? null;
if (is_string($pickNumbers)) {
$decoded = json_decode($pickNumbers, true);
$pickNumbers = is_array($decoded) ? $decoded : [];
}
if (!is_array($pickNumbers)) {
$pickNumbers = [];
}
if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) {
return '0.0000';
}
$unit = (string) ($bet['unit_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
return bcmul($unit, $odds, 4);
}
}

View File

@@ -15,12 +15,11 @@ class Channel extends Validate
'name' => 'require|max:255',
'agent_mode' => 'require|in:turnover,affiliate',
'status' => 'in:0,1',
'admin_id' => 'require|integer|gt:0',
'remark' => 'max:255',
];
protected $scene = [
'add' => ['code', 'name', 'agent_mode', 'status', 'admin_id', 'remark'],
'edit' => ['name', 'agent_mode', 'status', 'admin_id', 'remark'],
'add' => ['code', 'name', 'agent_mode', 'status', 'remark'],
'edit' => ['name', 'agent_mode', 'status', 'remark'],
];
}