1.优化奖励配置设置

This commit is contained in:
2026-06-02 14:51:10 +08:00
parent 3f97905ffa
commit 79c84c198a
10 changed files with 662 additions and 231 deletions

View File

@@ -263,6 +263,23 @@ class DiceRewardConfigController extends BaseController
}
}
/**
* 创建奖励对照(预览):不写入 dice_reward仅计算并返回预览分组数据。
* 若当前 dice_reward 与计算结果一致,则 unchanged=true并在预览中复用现有权重导入时仍沿用旧权重
*/
#[Permission('创建奖励对照', 'dice:reward_config:index:createRewardReference')]
public function createRewardReferencePreview(Request $request): Response
{
try {
$rewardLogic = new DiceRewardLogic();
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
$result = $rewardLogic->createRewardReferencePreviewFromConfig($deptId);
return $this->success($result, 'preview reward mapping success');
} catch (\plugin\saiadmin\exception\ApiException $e) {
return $this->fail($e->getMessage());
}
}
/**
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 N 次抽奖,返回各 grid_number 落点次数,可选保存到 dice_reward_config_record。
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID用于设定 T1-T5 概率

View File

@@ -327,6 +327,79 @@ class DiceRewardLogic
private const GRID_NUMBER_MIN = 5;
private const GRID_NUMBER_MAX = 30;
/**
* 预览:按当前 dice_reward_config 计算将要生成的 dice_reward不写库
* 若当前 dice_reward 与计算结果完全一致,则标记 unchanged=true并返回现有权重导入时将复用旧权重
*
* @return array{unchanged: bool, skipped: int, preview: array<string, array{0: array, 1: array}>}
* @throws ApiException
*/
public function createRewardReferencePreviewFromConfig(?int $deptId = null): array
{
$normalizedDeptId = $deptId;
$list = $this->loadConfigListForReference($normalizedDeptId);
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
$compare = $this->compareReferenceRows($computed['rows'], $existing);
$unchanged = $compare['unchanged'];
$previewRows = [];
foreach ($computed['rows'] as $row) {
$key = $row['direction'] . ':' . $row['grid_number'];
$weight = self::WEIGHT_MIN;
$oldStart = null;
$oldEnd = null;
$oldTier = null;
$oldWeight = null;
if (isset($existing[$key])) {
$oldStart = isset($existing[$key]['start_index']) ? (int) $existing[$key]['start_index'] : null;
$oldEnd = isset($existing[$key]['end_index']) ? (int) $existing[$key]['end_index'] : null;
$oldTier = isset($existing[$key]['tier']) ? (string) $existing[$key]['tier'] : null;
$oldWeight = isset($existing[$key]['weight']) ? (int) $existing[$key]['weight'] : null;
}
if ($unchanged && $oldWeight !== null) {
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $oldWeight));
}
$diffChanged = false;
$diffFields = [];
if ($oldStart === null || $oldEnd === null || $oldTier === null) {
$diffChanged = true;
$diffFields[] = 'new';
} else {
if ((int) $oldStart !== (int) ($row['start_index'] ?? 0)) {
$diffChanged = true;
$diffFields[] = 'start_index';
}
if ((int) $oldEnd !== (int) ($row['end_index'] ?? 0)) {
$diffChanged = true;
$diffFields[] = 'end_index';
}
if (trim((string) $oldTier) !== trim((string) ($row['tier'] ?? ''))) {
$diffChanged = true;
$diffFields[] = 'tier';
}
}
$previewRows[] = array_merge($row, [
'weight' => $weight,
'old_start_index' => $oldStart,
'old_end_index' => $oldEnd,
'old_tier' => $oldTier,
'old_weight' => $oldWeight,
'diff_changed' => $diffChanged,
'diff_fields' => $diffFields,
]);
}
return [
'unchanged' => $unchanged,
'skipped' => $computed['skipped'],
'preview' => $this->groupReferenceRowsByTierWithDirection($previewRows),
];
}
/**
* 创建奖励对照:先清空 dice_reward 表,再按两种方向为点数 5-30 生成记录。
*
@@ -352,10 +425,76 @@ class DiceRewardLogic
* @throws ApiException
*/
public function createRewardReferenceFromConfig(?int $deptId = null): array
{
$normalizedDeptId = $deptId;
$list = $this->loadConfigListForReference($normalizedDeptId);
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
$compare = $this->compareReferenceRows($computed['rows'], $existing);
if ($compare['unchanged']) {
return [
'created_clockwise' => 0,
'created_counterclockwise' => 0,
'updated_clockwise' => 0,
'updated_counterclockwise' => 0,
'skipped' => $computed['skipped'],
'unchanged' => true,
];
}
$table = (new DiceReward())->getTable();
if ($normalizedDeptId === null) {
Db::table($table)->whereNull('dept_id')->delete();
} else {
Db::table($table)->where('dept_id', $normalizedDeptId)->delete();
}
$createdCw = 0;
$createdCcw = 0;
foreach ($computed['rows'] as $row) {
$m = new DiceReward();
$m->tier = $row['tier'];
$m->direction = (int) $row['direction'];
$m->end_index = (int) $row['end_index'];
$m->weight = self::WEIGHT_MIN;
$m->grid_number = (int) $row['grid_number'];
$m->start_index = (int) $row['start_index'];
$m->ui_text = (string) ($row['ui_text'] ?? '');
$m->real_ev = $row['real_ev'] ?? null;
$m->remark = (string) ($row['remark'] ?? '');
$m->type = isset($row['type']) ? (int) $row['type'] : 0;
if ($normalizedDeptId !== null) {
$m->dept_id = $normalizedDeptId;
}
$m->save();
if ((int) $row['direction'] === DiceReward::DIRECTION_CLOCKWISE) {
$createdCw++;
} else {
$createdCcw++;
}
}
DiceReward::refreshCache($normalizedDeptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
return [
'created_clockwise' => $createdCw,
'created_counterclockwise' => $createdCcw,
'updated_clockwise' => 0,
'updated_counterclockwise' => 0,
'skipped' => $computed['skipped'],
'unchanged' => false,
];
}
/**
* 读取奖励配置(按 id asc并把模板 dept 转为 null
* @return array<int, array<string, mixed>>
*/
private function loadConfigListForReference(?int &$deptId): array
{
$configQuery = DiceRewardConfig::order('id', 'asc');
if ($deptId === null || $deptId === \app\dice\helper\AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
$templateId = \app\dice\helper\AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
$configQuery->where(function ($q) use ($templateId) {
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
});
@@ -367,25 +506,25 @@ class DiceRewardLogic
if (empty($list)) {
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
}
$configCount = count($list);
if ($configCount < self::BOARD_SIZE) {
if (count($list) < self::BOARD_SIZE) {
throw new ApiException(
\app\api\util\ApiLang::translateParams(
'奖励配置需覆盖 26 个格位id 0-25 或 1-26当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
[$configCount]
[count($list)]
)
);
}
return $list;
}
$table = (new DiceReward())->getTable();
if ($deptId === null) {
Db::table($table)->whereNull('dept_id')->delete();
} else {
Db::table($table)->where('dept_id', $deptId)->delete();
}
DiceReward::refreshCache($deptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos],避免 config.id 非 0-25/1-26 时取模结果找不到
/**
* 计算 5-30 两个方向的对照行(不含权重)
* @param array<int, array<string, mixed>> $list
* @return array{rows: array<int, array<string, mixed>>, skipped: int}
*/
private function computeReferenceRowsFromConfigList(array $list, ?int $deptId): array
{
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos]
$gridToPosition = [];
foreach ($list as $pos => $row) {
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
@@ -394,12 +533,8 @@ class DiceRewardLogic
}
}
$createdCw = 0;
$createdCcw = 0;
$updatedCw = 0;
$updatedCcw = 0;
$rows = [];
$skipped = 0;
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
if (!isset($gridToPosition[$gridNumber])) {
$skipped++;
@@ -414,115 +549,131 @@ class DiceRewardLogic
$configCw = $list[$endPosCw] ?? null;
$configCcw = $list[$endPosCcw] ?? null;
$endIdCw = $configCw !== null && isset($configCw['id']) ? (int) $configCw['id'] : 0;
$endIdCcw = $configCcw !== null && isset($configCcw['id']) ? (int) $configCcw['id'] : 0;
if ($configCw !== null) {
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
if ($tier !== '') {
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
$weightCw = isset($configCw['weight']) && $configCw['weight'] !== null
? $configCw['weight']
: self::WEIGHT_MIN;
$payloadCw = [
$rows[] = [
'tier' => $tier,
'weight' => $weightCw,
'direction' => DiceReward::DIRECTION_CLOCKWISE,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => $endIdCw,
'end_index' => isset($configCw['id']) ? (int) $configCw['id'] : 0,
'ui_text' => $configCw['ui_text'] ?? '',
'real_ev' => $configCw['real_ev'] ?? null,
'remark' => $configCw['remark'] ?? '',
'type' => isset($configCw['type']) ? (int) $configCw['type'] : 0,
];
$existingQuery = DiceReward::where('direction', DiceReward::DIRECTION_CLOCKWISE)->where('grid_number', $gridNumber);
if ($deptId === null) {
$existingQuery->whereNull('dept_id');
} else {
$existingQuery->where('dept_id', $deptId);
}
$existing = $existingQuery->find();
if ($existing) {
DiceReward::where('id', $existing->id)->update($payloadCw);
$updatedCw++;
} else {
$m = new DiceReward();
$m->tier = $tier;
$m->direction = DiceReward::DIRECTION_CLOCKWISE;
$m->end_index = $endIdCw;
$m->weight = $weightCw;
$m->grid_number = $gridNumber;
$m->start_index = $startId;
$m->ui_text = $configCw['ui_text'] ?? '';
$m->real_ev = $configCw['real_ev'] ?? null;
$m->remark = $configCw['remark'] ?? '';
$m->type = isset($configCw['type']) ? (int) $configCw['type'] : 0;
if ($deptId !== null) {
$m->dept_id = $deptId;
}
$m->save();
$createdCw++;
}
}
}
if ($configCcw !== null) {
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
if ($tier !== '') {
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
$weightCcw = isset($configCcw['weight']) && $configCcw['weight'] !== null
? $configCcw['weight']
: self::WEIGHT_MIN;
$payloadCcw = [
$rows[] = [
'tier' => $tier,
'weight' => $weightCcw,
'direction' => DiceReward::DIRECTION_COUNTERCLOCKWISE,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => $endIdCcw,
'end_index' => isset($configCcw['id']) ? (int) $configCcw['id'] : 0,
'ui_text' => $configCcw['ui_text'] ?? '',
'real_ev' => $configCcw['real_ev'] ?? null,
'remark' => $configCcw['remark'] ?? '',
'type' => isset($configCcw['type']) ? (int) $configCcw['type'] : 0,
];
$existingQuery = DiceReward::where('direction', DiceReward::DIRECTION_COUNTERCLOCKWISE)->where('grid_number', $gridNumber);
if ($deptId === null) {
$existingQuery->whereNull('dept_id');
} else {
$existingQuery->where('dept_id', $deptId);
}
$existing = $existingQuery->find();
if ($existing) {
DiceReward::where('id', $existing->id)->update($payloadCcw);
$updatedCcw++;
} else {
$m = new DiceReward();
$m->tier = $tier;
$m->direction = DiceReward::DIRECTION_COUNTERCLOCKWISE;
$m->end_index = $endIdCcw;
$m->weight = $weightCcw;
$m->grid_number = $gridNumber;
$m->start_index = $startId;
$m->ui_text = $configCcw['ui_text'] ?? '';
$m->real_ev = $configCcw['real_ev'] ?? null;
$m->remark = $configCcw['remark'] ?? '';
$m->type = isset($configCcw['type']) ? (int) $configCcw['type'] : 0;
if ($deptId !== null) {
$m->dept_id = $deptId;
}
$m->save();
$createdCcw++;
}
}
}
}
return ['rows' => $rows, 'skipped' => $skipped];
}
DiceReward::refreshCache($deptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
return [
'created_clockwise' => $createdCw,
'created_counterclockwise' => $createdCcw,
'updated_clockwise' => $updatedCw,
'updated_counterclockwise' => $updatedCcw,
'skipped' => $skipped,
];
/**
* 读出当前 dice_reward用于对比/复用权重。key = "direction:grid_number"
* @return array<string, array<string, mixed>>
*/
private function loadExistingRewardRowsForReference(?int $deptId): array
{
$query = DiceReward::whereIn('grid_number', range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX))
->whereIn('direction', [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE]);
if ($deptId === null) {
$query->whereNull('dept_id');
} else {
$query->where('dept_id', $deptId);
}
$rows = $query->select()->toArray();
$map = [];
foreach ($rows as $r) {
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
$gn = isset($r['grid_number']) ? (int) $r['grid_number'] : 0;
$map[$dir . ':' . $gn] = $r;
}
return $map;
}
/**
* 对比 computed 与 existing 是否完全一致(忽略权重)
* @param array<int, array<string, mixed>> $computedRows
* @param array<string, array<string, mixed>> $existingMap
* @return array{unchanged: bool}
*/
private function compareReferenceRows(array $computedRows, array $existingMap): array
{
if (empty($computedRows)) {
return ['unchanged' => false];
}
foreach ($computedRows as $row) {
$key = $row['direction'] . ':' . $row['grid_number'];
if (!isset($existingMap[$key])) {
return ['unchanged' => false];
}
$ex = $existingMap[$key];
$same =
((int) ($ex['start_index'] ?? 0) === (int) ($row['start_index'] ?? 0)) &&
((int) ($ex['end_index'] ?? 0) === (int) ($row['end_index'] ?? 0)) &&
(trim((string) ($ex['tier'] ?? '')) === trim((string) ($row['tier'] ?? '')));
if (!$same) {
return ['unchanged' => false];
}
}
return ['unchanged' => true];
}
/**
* 将行按 tier -> {0:[],1:[]} 组织,便于前端展示(与 weightRatioList 输出结构一致)
* @param array<int, array<string, mixed>> $rows
* @return array<string, array{0: array, 1: array}>
*/
private function groupReferenceRowsByTierWithDirection(array $rows): array
{
$result = [];
foreach (self::TIER_KEYS as $tier) {
$result[$tier] = [0 => [], 1 => []];
}
foreach ($rows as $r) {
$tier = isset($r['tier']) ? trim((string) $r['tier']) : '';
if ($tier === '' || !isset($result[$tier])) {
continue;
}
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
$dir = $dir === 1 ? 1 : 0;
$result[$tier][$dir][] = [
'reward_id' => 0,
'id' => isset($r['end_index']) ? (int) $r['end_index'] : 0,
'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
'ui_text' => (string) ($r['ui_text'] ?? ''),
'real_ev' => $r['real_ev'] ?? 0,
'remark' => (string) ($r['remark'] ?? ''),
'weight' => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
'start_index' => isset($r['start_index']) ? (int) $r['start_index'] : 0,
'tier' => (string) ($r['tier'] ?? ''),
'old_start_index' => $r['old_start_index'] ?? null,
'old_end_index' => $r['old_end_index'] ?? null,
'old_tier' => $r['old_tier'] ?? null,
'old_weight' => $r['old_weight'] ?? null,
'diff_changed' => (bool) ($r['diff_changed'] ?? false),
'diff_fields' => $r['diff_fields'] ?? [],
];
}
return $result;
}
}